From dff2d8111ca7091f01879bd3fa1f5303a90a2d3d Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Sat, 3 Jun 2023 20:39:32 -0400 Subject: [PATCH 01/10] Android: Convert CustomTitleView to Kotlin --- .../dolphinemu/ui/main/CustomTitleView.java | 85 ------------------- .../dolphinemu/ui/main/CustomTitleView.kt | 53 ++++++++++++ 2 files changed, 53 insertions(+), 85 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/CustomTitleView.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/CustomTitleView.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/CustomTitleView.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/CustomTitleView.java deleted file mode 100644 index 08e4777303..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/CustomTitleView.java +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.ui.main; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.leanback.widget.TitleViewAdapter; - -import org.dolphinemu.dolphinemu.R; - -public class CustomTitleView extends LinearLayout implements TitleViewAdapter.Provider -{ - private final TextView mTitleView; - private final View mBadgeView; - - private final TitleViewAdapter mTitleViewAdapter = new TitleViewAdapter() - { - @Override - public View getSearchAffordanceView() - { - return null; - } - - @Override - public void setTitle(CharSequence titleText) - { - CustomTitleView.this.setTitle(titleText); - } - - @Override - public void setBadgeDrawable(Drawable drawable) - { - CustomTitleView.this.setBadgeDrawable(drawable); - } - }; - - public CustomTitleView(Context context) - { - this(context, null); - } - - public CustomTitleView(Context context, AttributeSet attrs) - { - this(context, attrs, 0); - } - - public CustomTitleView(Context context, AttributeSet attrs, int defStyle) - { - super(context, attrs, defStyle); - View root = LayoutInflater.from(context).inflate(R.layout.tv_title, this); - mTitleView = root.findViewById(R.id.title); - mBadgeView = root.findViewById(R.id.badge); - } - - public void setTitle(CharSequence title) - { - if (title != null) - { - mTitleView.setText(title); - mTitleView.setVisibility(View.VISIBLE); - mBadgeView.setVisibility(View.VISIBLE); - } - } - - public void setBadgeDrawable(Drawable drawable) - { - if (drawable != null) - { - mTitleView.setVisibility(View.GONE); - mBadgeView.setVisibility(View.VISIBLE); - } - } - - @Override - public TitleViewAdapter getTitleViewAdapter() - { - return mTitleViewAdapter; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/CustomTitleView.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/CustomTitleView.kt new file mode 100644 index 0000000000..35f69195e9 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/CustomTitleView.kt @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.main + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.leanback.widget.TitleViewAdapter +import org.dolphinemu.dolphinemu.R + +class CustomTitleView @JvmOverloads constructor( + context: Context?, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : LinearLayout(context, attrs, defStyle), TitleViewAdapter.Provider { + private val titleView: TextView + private val badgeView: View + private val titleViewAdapter: TitleViewAdapter = object : TitleViewAdapter() { + override fun getSearchAffordanceView(): View? = null + + override fun setTitle(titleText: CharSequence?) = this@CustomTitleView.setTitle(titleText) + + override fun setBadgeDrawable(drawable: Drawable?) = + this@CustomTitleView.setBadgeDrawable(drawable) + } + + init { + val root = LayoutInflater.from(context).inflate(R.layout.tv_title, this) + titleView = root.findViewById(R.id.title) + badgeView = root.findViewById(R.id.badge) + } + + fun setTitle(title: CharSequence?) { + if (title != null) { + titleView.text = title + titleView.visibility = VISIBLE + badgeView.visibility = VISIBLE + } + } + + fun setBadgeDrawable(drawable: Drawable?) { + if (drawable != null) { + titleView.visibility = GONE + badgeView.visibility = VISIBLE + } + } + + override fun getTitleViewAdapter(): TitleViewAdapter = titleViewAdapter +} From 18a03e69a9d74ab32f7d63323df9ffa65f964d13 Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Sat, 3 Jun 2023 20:39:44 -0400 Subject: [PATCH 02/10] Android: Convert MainActivity to Kotlin --- .../dolphinemu/ui/main/MainActivity.java | 441 ------------------ .../dolphinemu/ui/main/MainActivity.kt | 332 +++++++++++++ 2 files changed, 332 insertions(+), 441 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java deleted file mode 100644 index 488f0909df..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java +++ /dev/null @@ -1,441 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.ui.main; - -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.graphics.Insets; -import androidx.core.splashscreen.SplashScreen; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowCompat; -import androidx.core.view.WindowInsetsCompat; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import com.google.android.material.color.MaterialColors; -import com.google.android.material.tabs.TabLayout; - -import org.dolphinemu.dolphinemu.fragments.GridOptionDialogFragment; -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.activities.EmulationActivity; -import org.dolphinemu.dolphinemu.adapters.PlatformPagerAdapter; -import org.dolphinemu.dolphinemu.databinding.ActivityMainBinding; -import org.dolphinemu.dolphinemu.features.settings.model.IntSetting; -import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig; -import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag; -import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity; -import org.dolphinemu.dolphinemu.services.GameFileCacheManager; -import org.dolphinemu.dolphinemu.ui.platform.Platform; -import org.dolphinemu.dolphinemu.ui.platform.PlatformGamesView; -import org.dolphinemu.dolphinemu.utils.Action1; -import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner; -import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; -import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; -import org.dolphinemu.dolphinemu.utils.InsetsHelper; -import org.dolphinemu.dolphinemu.utils.PermissionsHandler; -import org.dolphinemu.dolphinemu.utils.StartupHandler; -import org.dolphinemu.dolphinemu.utils.ThemeHelper; -import org.dolphinemu.dolphinemu.utils.WiiUtils; - -/** - * The main Activity of the Lollipop style UI. Manages several PlatformGamesFragments, which - * individually display a grid of available games for each Fragment, in a tabbed layout. - */ -public final class MainActivity extends AppCompatActivity - implements MainView, SwipeRefreshLayout.OnRefreshListener, ThemeProvider -{ - private int mThemeId; - - private final MainPresenter mPresenter = new MainPresenter(this, this); - - private ActivityMainBinding mBinding; - - @Override - protected void onCreate(Bundle savedInstanceState) - { - SplashScreen splashScreen = SplashScreen.installSplashScreen(this); - splashScreen.setKeepOnScreenCondition( - () -> !DirectoryInitialization.areDolphinDirectoriesReady()); - - ThemeHelper.setTheme(this); - - super.onCreate(savedInstanceState); - - mBinding = ActivityMainBinding.inflate(getLayoutInflater()); - setContentView(mBinding.getRoot()); - - WindowCompat.setDecorFitsSystemWindows(getWindow(), false); - setInsets(); - ThemeHelper.enableStatusBarScrollTint(this, mBinding.appbarMain); - - mBinding.toolbarMain.setTitle(R.string.app_name); - setSupportActionBar(mBinding.toolbarMain); - - // Set up the FAB. - mBinding.buttonAddDirectory.setOnClickListener(view -> mPresenter.onFabClick()); - mBinding.appbarMain.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> - { - if (verticalOffset == 0) - { - mBinding.buttonAddDirectory.extend(); - } - else if (appBarLayout.getTotalScrollRange() == -verticalOffset) - { - mBinding.buttonAddDirectory.shrink(); - } - }); - - mPresenter.onCreate(); - - // Stuff in this block only happens when this activity is newly created (i.e. not a rotation) - if (savedInstanceState == null) - { - StartupHandler.HandleInit(this); - new AfterDirectoryInitializationRunner().runWithLifecycle(this, this::checkTheme); - } - - if (!DirectoryInitialization.isWaitingForWriteAccess(this)) - { - new AfterDirectoryInitializationRunner() - .runWithLifecycle(this, this::setPlatformTabsAndStartGameFileCacheService); - } - } - - @Override - protected void onResume() - { - ThemeHelper.setCorrectTheme(this); - - super.onResume(); - - if (DirectoryInitialization.shouldStart(this)) - { - DirectoryInitialization.start(this); - new AfterDirectoryInitializationRunner() - .runWithLifecycle(this, this::setPlatformTabsAndStartGameFileCacheService); - } - - mPresenter.onResume(); - } - - @Override - protected void onDestroy() - { - super.onDestroy(); - mPresenter.onDestroy(); - } - - @Override - protected void onStart() - { - super.onStart(); - StartupHandler.checkSessionReset(this); - } - - @Override - protected void onStop() - { - super.onStop(); - - if (isChangingConfigurations()) - { - MainPresenter.skipRescanningLibrary(); - } - else if (DirectoryInitialization.areDolphinDirectoriesReady()) - { - // If the currently selected platform tab changed, save it to disk - NativeConfig.save(NativeConfig.LAYER_BASE); - } - - StartupHandler.setSessionTime(this); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) - { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.menu_game_grid, menu); - - if (WiiUtils.isSystemMenuInstalled()) - { - int resId = WiiUtils.isSystemMenuvWii() ? - R.string.grid_menu_load_vwii_system_menu_installed : - R.string.grid_menu_load_wii_system_menu_installed; - - menu.findItem(R.id.menu_load_wii_system_menu).setTitle( - getString(resId, WiiUtils.getSystemMenuVersion())); - } - - return true; - } - - /** - * MainView - */ - - @Override - public void setVersionString(String version) - { - mBinding.toolbarMain.setSubtitle(version); - } - - @Override - public void launchSettingsActivity(MenuTag menuTag) - { - SettingsActivity.launch(this, menuTag); - } - - @Override - public void launchFileListActivity() - { - if (DirectoryInitialization.preferOldFolderPicker(this)) - { - FileBrowserHelper.openDirectoryPicker(this, FileBrowserHelper.GAME_EXTENSIONS); - } - else - { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - startActivityForResult(intent, MainPresenter.REQUEST_DIRECTORY); - } - } - - @Override - public void launchOpenFileActivity(int requestCode) - { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); - startActivityForResult(intent, requestCode); - } - - /** - * @param requestCode An int describing whether the Activity that is returning did so successfully. - * @param resultCode An int describing what Activity is giving us this callback. - * @param result The information the returning Activity is providing us. - */ - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent result) - { - super.onActivityResult(requestCode, resultCode, result); - - // If the user picked a file, as opposed to just backing out. - if (resultCode == RESULT_OK) - { - Uri uri = result.getData(); - switch (requestCode) - { - case MainPresenter.REQUEST_DIRECTORY: - if (DirectoryInitialization.preferOldFolderPicker(this)) - { - mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedPath(result)); - } - else - { - mPresenter.onDirectorySelected(result); - } - break; - - case MainPresenter.REQUEST_GAME_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, uri, - FileBrowserHelper.GAME_LIKE_EXTENSIONS, - () -> EmulationActivity.launch(this, result.getData().toString(), false)); - break; - - case MainPresenter.REQUEST_WAD_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.WAD_EXTENSION, - () -> mPresenter.installWAD(result.getData().toString())); - break; - - case MainPresenter.REQUEST_WII_SAVE_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.BIN_EXTENSION, - () -> mPresenter.importWiiSave(result.getData().toString())); - break; - - case MainPresenter.REQUEST_NAND_BIN_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.BIN_EXTENSION, - () -> mPresenter.importNANDBin(result.getData().toString())); - break; - } - } - else - { - MainPresenter.skipRescanningLibrary(); - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) - { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - if (requestCode == PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION) - { - if (grantResults[0] == PackageManager.PERMISSION_DENIED) - { - PermissionsHandler.setWritePermissionDenied(); - } - - DirectoryInitialization.start(this); - new AfterDirectoryInitializationRunner() - .runWithLifecycle(this, this::setPlatformTabsAndStartGameFileCacheService); - } - } - - /** - * Called by the framework whenever any actionbar/toolbar icon is clicked. - * - * @param item The icon that was clicked on. - * @return True if the event was handled, false to bubble it up to the OS. - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) - { - return mPresenter.handleOptionSelection(item.getItemId(), this); - } - - /** - * Called when the user requests a refresh by swiping down. - */ - @Override - public void onRefresh() - { - setRefreshing(true); - GameFileCacheManager.startRescan(); - } - - /** - * Shows or hides the loading indicator. - */ - @Override - public void setRefreshing(boolean refreshing) - { - forEachPlatformGamesView(view -> view.setRefreshing(refreshing)); - } - - /** - * To be called when the game file cache is updated. - */ - @Override - public void showGames() - { - forEachPlatformGamesView(PlatformGamesView::showGames); - } - - @Override - public void reloadGrid() - { - forEachPlatformGamesView(PlatformGamesView::refetchMetadata); - } - - @Override - public void showGridOptions() - { - new GridOptionDialogFragment().show(getSupportFragmentManager(), "gridOptions"); - } - - private void forEachPlatformGamesView(Action1 action) - { - for (Platform platform : Platform.values()) - { - PlatformGamesView fragment = getPlatformGamesView(platform); - if (fragment != null) - { - action.call(fragment); - } - } - } - - @Nullable - private PlatformGamesView getPlatformGamesView(Platform platform) - { - String fragmentTag = - "android:switcher:" + mBinding.pagerPlatforms.getId() + ":" + platform.toInt(); - - return (PlatformGamesView) getSupportFragmentManager().findFragmentByTag(fragmentTag); - } - - // Don't call this before DirectoryInitialization completes. - private void setPlatformTabsAndStartGameFileCacheService() - { - PlatformPagerAdapter platformPagerAdapter = new PlatformPagerAdapter( - getSupportFragmentManager(), this); - mBinding.pagerPlatforms.setAdapter(platformPagerAdapter); - mBinding.pagerPlatforms.setOffscreenPageLimit(platformPagerAdapter.getCount()); - mBinding.tabsPlatforms.setupWithViewPager(mBinding.pagerPlatforms); - mBinding.tabsPlatforms.addOnTabSelectedListener( - new TabLayout.ViewPagerOnTabSelectedListener(mBinding.pagerPlatforms) - { - @Override - public void onTabSelected(@NonNull TabLayout.Tab tab) - { - super.onTabSelected(tab); - IntSetting.MAIN_LAST_PLATFORM_TAB.setInt(NativeConfig.LAYER_BASE, - tab.getPosition()); - } - }); - - for (int i = 0; i < PlatformPagerAdapter.TAB_ICONS.length; i++) - { - mBinding.tabsPlatforms.getTabAt(i).setIcon(PlatformPagerAdapter.TAB_ICONS[i]); - } - - mBinding.pagerPlatforms.setCurrentItem(IntSetting.MAIN_LAST_PLATFORM_TAB.getInt()); - - showGames(); - GameFileCacheManager.startLoad(); - } - - @Override - public void setTheme(int themeId) - { - super.setTheme(themeId); - this.mThemeId = themeId; - } - - @Override - public int getThemeId() - { - return mThemeId; - } - - private void checkTheme() - { - ThemeHelper.setCorrectTheme(this); - } - - private void setInsets() - { - ViewCompat.setOnApplyWindowInsetsListener(mBinding.appbarMain, (v, windowInsets) -> - { - Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); - - InsetsHelper.insetAppBar(insets, mBinding.appbarMain); - - ViewGroup.MarginLayoutParams mlpFab = - (ViewGroup.MarginLayoutParams) mBinding.buttonAddDirectory.getLayoutParams(); - int fabPadding = getResources().getDimensionPixelSize(R.dimen.spacing_large); - mlpFab.leftMargin = insets.left + fabPadding; - mlpFab.bottomMargin = insets.bottom + fabPadding; - mlpFab.rightMargin = insets.right + fabPadding; - mBinding.buttonAddDirectory.setLayoutParams(mlpFab); - - mBinding.pagerPlatforms.setPadding(insets.left, 0, insets.right, 0); - - InsetsHelper.applyNavbarWorkaround(insets.bottom, mBinding.workaroundView); - ThemeHelper.setNavigationBarColor(this, - MaterialColors.getColor(mBinding.appbarMain, R.attr.colorSurface)); - - return windowInsets; - }); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt new file mode 100644 index 0000000000..3a003e0574 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.main + +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup.MarginLayoutParams +import androidx.appcompat.app.AppCompatActivity +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.color.MaterialColors +import com.google.android.material.tabs.TabLayout +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.adapters.PlatformPagerAdapter +import org.dolphinemu.dolphinemu.databinding.ActivityMainBinding +import org.dolphinemu.dolphinemu.features.settings.model.IntSetting +import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig +import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag +import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity +import org.dolphinemu.dolphinemu.fragments.GridOptionDialogFragment +import org.dolphinemu.dolphinemu.services.GameFileCacheManager +import org.dolphinemu.dolphinemu.ui.platform.Platform +import org.dolphinemu.dolphinemu.ui.platform.PlatformGamesView +import org.dolphinemu.dolphinemu.utils.Action1 +import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner +import org.dolphinemu.dolphinemu.utils.DirectoryInitialization +import org.dolphinemu.dolphinemu.utils.FileBrowserHelper +import org.dolphinemu.dolphinemu.utils.InsetsHelper +import org.dolphinemu.dolphinemu.utils.PermissionsHandler +import org.dolphinemu.dolphinemu.utils.StartupHandler +import org.dolphinemu.dolphinemu.utils.ThemeHelper +import org.dolphinemu.dolphinemu.utils.WiiUtils + +class MainActivity : AppCompatActivity(), MainView, OnRefreshListener, ThemeProvider { + override var themeId = 0 + + private val presenter = MainPresenter(this, this) + + private lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen().setKeepOnScreenCondition { !DirectoryInitialization.areDolphinDirectoriesReady() } + + ThemeHelper.setTheme(this) + + super.onCreate(savedInstanceState) + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + WindowCompat.setDecorFitsSystemWindows(window, false) + setInsets() + ThemeHelper.enableStatusBarScrollTint(this, binding.appbarMain) + + binding.toolbarMain.setTitle(R.string.app_name) + setSupportActionBar(binding.toolbarMain) + + // Set up the FAB. + binding.buttonAddDirectory.setOnClickListener { presenter.onFabClick() } + binding.appbarMain.addOnOffsetChangedListener { appBarLayout: AppBarLayout, verticalOffset: Int -> + if (verticalOffset == 0) { + binding.buttonAddDirectory.extend() + } else if (appBarLayout.totalScrollRange == -verticalOffset) { + binding.buttonAddDirectory.shrink() + } + } + + presenter.onCreate() + + // Stuff in this block only happens when this activity is newly created (i.e. not a rotation) + if (savedInstanceState == null) { + StartupHandler.HandleInit(this) + AfterDirectoryInitializationRunner().runWithLifecycle(this) { + ThemeHelper.setCorrectTheme(this) + } + } + if (!DirectoryInitialization.isWaitingForWriteAccess(this)) { + AfterDirectoryInitializationRunner() + .runWithLifecycle(this) { setPlatformTabsAndStartGameFileCacheService() } + } + } + + override fun onResume() { + ThemeHelper.setCorrectTheme(this) + + super.onResume() + if (DirectoryInitialization.shouldStart(this)) { + DirectoryInitialization.start(this) + AfterDirectoryInitializationRunner() + .runWithLifecycle(this) { setPlatformTabsAndStartGameFileCacheService() } + } + + presenter.onResume() + } + + override fun onDestroy() { + super.onDestroy() + presenter.onDestroy() + } + + override fun onStart() { + super.onStart() + StartupHandler.checkSessionReset(this) + } + + override fun onStop() { + super.onStop() + if (isChangingConfigurations) { + MainPresenter.skipRescanningLibrary() + } else if (DirectoryInitialization.areDolphinDirectoriesReady()) { + // If the currently selected platform tab changed, save it to disk + NativeConfig.save(NativeConfig.LAYER_BASE) + } + + StartupHandler.setSessionTime(this) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_game_grid, menu) + if (WiiUtils.isSystemMenuInstalled()) { + val resId = + if (WiiUtils.isSystemMenuvWii()) R.string.grid_menu_load_vwii_system_menu_installed else R.string.grid_menu_load_wii_system_menu_installed + menu.findItem(R.id.menu_load_wii_system_menu).title = + getString(resId, WiiUtils.getSystemMenuVersion()) + } + return true + } + + /** + * MainView + */ + override fun setVersionString(version: String) { + binding.toolbarMain.subtitle = version + } + + override fun launchSettingsActivity(menuTag: MenuTag?) { + SettingsActivity.launch(this, menuTag) + } + + override fun launchFileListActivity() { + if (DirectoryInitialization.preferOldFolderPicker(this)) { + FileBrowserHelper.openDirectoryPicker(this, FileBrowserHelper.GAME_EXTENSIONS) + } else { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + startActivityForResult(intent, MainPresenter.REQUEST_DIRECTORY) + } + } + + override fun launchOpenFileActivity(requestCode: Int) { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "*/*" + startActivityForResult(intent, requestCode) + } + + /** + * @param requestCode An int describing whether the Activity that is returning did so successfully. + * @param resultCode An int describing what Activity is giving us this callback. + * @param result The information the returning Activity is providing us. + */ + override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) { + super.onActivityResult(requestCode, resultCode, result) + + // If the user picked a file, as opposed to just backing out. + if (resultCode == RESULT_OK) { + val uri = result!!.data + when (requestCode) { + MainPresenter.REQUEST_DIRECTORY -> { + if (DirectoryInitialization.preferOldFolderPicker(this)) { + presenter.onDirectorySelected(FileBrowserHelper.getSelectedPath(result)) + } else { + presenter.onDirectorySelected(result) + } + } + + MainPresenter.REQUEST_GAME_FILE -> FileBrowserHelper.runAfterExtensionCheck( + this, uri, FileBrowserHelper.GAME_LIKE_EXTENSIONS + ) { EmulationActivity.launch(this, result.data.toString(), false) } + + MainPresenter.REQUEST_WAD_FILE -> FileBrowserHelper.runAfterExtensionCheck( + this, uri, FileBrowserHelper.WAD_EXTENSION + ) { presenter.installWAD(result.data.toString()) } + + MainPresenter.REQUEST_WII_SAVE_FILE -> FileBrowserHelper.runAfterExtensionCheck( + this, uri, FileBrowserHelper.BIN_EXTENSION + ) { presenter.importWiiSave(result.data.toString()) } + + MainPresenter.REQUEST_NAND_BIN_FILE -> FileBrowserHelper.runAfterExtensionCheck( + this, uri, FileBrowserHelper.BIN_EXTENSION + ) { presenter.importNANDBin(result.data.toString()) } + } + } else { + MainPresenter.skipRescanningLibrary() + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION) { + if (grantResults[0] == PackageManager.PERMISSION_DENIED) { + PermissionsHandler.setWritePermissionDenied() + } + + DirectoryInitialization.start(this) + AfterDirectoryInitializationRunner() + .runWithLifecycle(this) { setPlatformTabsAndStartGameFileCacheService() } + } + } + + /** + * Called by the framework whenever any actionbar/toolbar icon is clicked. + * + * @param item The icon that was clicked on. + * @return True if the event was handled, false to bubble it up to the OS. + */ + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return presenter.handleOptionSelection(item.itemId, this) + } + + /** + * Called when the user requests a refresh by swiping down. + */ + override fun onRefresh() { + setRefreshing(true) + GameFileCacheManager.startRescan() + } + + /** + * Shows or hides the loading indicator. + */ + override fun setRefreshing(refreshing: Boolean) = + forEachPlatformGamesView { view: PlatformGamesView -> view.setRefreshing(refreshing) } + + /** + * To be called when the game file cache is updated. + */ + override fun showGames() = + forEachPlatformGamesView { obj: PlatformGamesView -> obj.showGames() } + + override fun reloadGrid() { + forEachPlatformGamesView { obj: PlatformGamesView -> obj.refetchMetadata() } + } + + override fun showGridOptions() = + GridOptionDialogFragment().show(supportFragmentManager, "gridOptions") + + private fun forEachPlatformGamesView(action: Action1) { + for (platform in Platform.values()) { + val fragment = getPlatformGamesView(platform) + if (fragment != null) { + action.call(fragment) + } + } + } + + private fun getPlatformGamesView(platform: Platform): PlatformGamesView? { + val fragmentTag = + "android:switcher:" + binding.pagerPlatforms.id + ":" + platform.toInt() + return supportFragmentManager.findFragmentByTag(fragmentTag) as PlatformGamesView? + } + + // Don't call this before DirectoryInitialization completes. + private fun setPlatformTabsAndStartGameFileCacheService() { + val platformPagerAdapter = PlatformPagerAdapter( + supportFragmentManager, this + ) + binding.pagerPlatforms.adapter = platformPagerAdapter + binding.pagerPlatforms.offscreenPageLimit = platformPagerAdapter.count + binding.tabsPlatforms.setupWithViewPager(binding.pagerPlatforms) + binding.tabsPlatforms.addOnTabSelectedListener( + object : TabLayout.ViewPagerOnTabSelectedListener(binding.pagerPlatforms) { + override fun onTabSelected(tab: TabLayout.Tab) { + super.onTabSelected(tab) + IntSetting.MAIN_LAST_PLATFORM_TAB.setInt( + NativeConfig.LAYER_BASE, + tab.position + ) + } + }) + + for (i in PlatformPagerAdapter.TAB_ICONS.indices) { + binding.tabsPlatforms.getTabAt(i)?.setIcon(PlatformPagerAdapter.TAB_ICONS[i]) + } + + binding.pagerPlatforms.currentItem = IntSetting.MAIN_LAST_PLATFORM_TAB.int + + showGames() + GameFileCacheManager.startLoad() + } + + override fun setTheme(themeId: Int) { + super.setTheme(themeId) + this.themeId = themeId + } + + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener(binding.appbarMain) { _: View?, windowInsets: WindowInsetsCompat -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + + InsetsHelper.insetAppBar(insets, binding.appbarMain) + + val mlpFab = binding.buttonAddDirectory.layoutParams as MarginLayoutParams + val fabPadding = resources.getDimensionPixelSize(R.dimen.spacing_large) + mlpFab.leftMargin = insets.left + fabPadding + mlpFab.bottomMargin = insets.bottom + fabPadding + mlpFab.rightMargin = insets.right + fabPadding + binding.buttonAddDirectory.layoutParams = mlpFab + + binding.pagerPlatforms.setPadding(insets.left, 0, insets.right, 0) + + InsetsHelper.applyNavbarWorkaround(insets.bottom, binding.workaroundView) + ThemeHelper.setNavigationBarColor( + this, + MaterialColors.getColor(binding.appbarMain, R.attr.colorSurface) + ) + + windowInsets + } +} From 01d4e6fe87aa09ce19d9ef3dddd057c65b322bc7 Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Sat, 3 Jun 2023 20:39:55 -0400 Subject: [PATCH 03/10] Android: Convert MainPresenter to Kotlin --- .../dolphinemu/ui/main/MainActivity.kt | 5 - .../dolphinemu/ui/main/MainPresenter.java | 349 ------------------ .../dolphinemu/ui/main/MainPresenter.kt | 311 ++++++++++++++++ 3 files changed, 311 insertions(+), 354 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt index 3a003e0574..84d870c921 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt @@ -102,11 +102,6 @@ class MainActivity : AppCompatActivity(), MainView, OnRefreshListener, ThemeProv presenter.onResume() } - override fun onDestroy() { - super.onDestroy() - presenter.onDestroy() - } - override fun onStart() { super.onStart() StartupHandler.checkSessionReset(this) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java deleted file mode 100644 index e136864faa..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java +++ /dev/null @@ -1,349 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.ui.main; - -import android.content.ContentResolver; -import android.content.Intent; -import android.net.Uri; - -import androidx.activity.ComponentActivity; -import androidx.fragment.app.FragmentActivity; -import androidx.lifecycle.Observer; -import androidx.lifecycle.ViewModelProvider; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import org.dolphinemu.dolphinemu.BuildConfig; -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.activities.EmulationActivity; -import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; -import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag; -import org.dolphinemu.dolphinemu.features.sysupdate.ui.SystemUpdateProgressBarDialogFragment; -import org.dolphinemu.dolphinemu.features.sysupdate.ui.SystemMenuNotInstalledDialogFragment; -import org.dolphinemu.dolphinemu.features.sysupdate.ui.SystemUpdateViewModel; -import org.dolphinemu.dolphinemu.fragments.AboutDialogFragment; -import org.dolphinemu.dolphinemu.model.GameFileCache; -import org.dolphinemu.dolphinemu.services.GameFileCacheManager; -import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner; -import org.dolphinemu.dolphinemu.utils.BooleanSupplier; -import org.dolphinemu.dolphinemu.utils.CompletableFuture; -import org.dolphinemu.dolphinemu.utils.ContentHandler; -import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; -import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; -import org.dolphinemu.dolphinemu.utils.PermissionsHandler; -import org.dolphinemu.dolphinemu.utils.ThreadUtil; -import org.dolphinemu.dolphinemu.utils.WiiUtils; - -import java.util.Arrays; -import java.util.concurrent.ExecutionException; - -public final class MainPresenter -{ - public static final int REQUEST_DIRECTORY = 1; - public static final int REQUEST_GAME_FILE = 2; - public static final int REQUEST_SD_FILE = 3; - public static final int REQUEST_WAD_FILE = 4; - public static final int REQUEST_WII_SAVE_FILE = 5; - public static final int REQUEST_NAND_BIN_FILE = 6; - - private static boolean sShouldRescanLibrary = true; - - private final MainView mView; - private final FragmentActivity mActivity; - private String mDirToAdd; - - public MainPresenter(MainView view, FragmentActivity activity) - { - mView = view; - mActivity = activity; - } - - public void onCreate() - { - // Ask the user to grant write permission if relevant and not already granted - if (DirectoryInitialization.isWaitingForWriteAccess(mActivity)) - PermissionsHandler.requestWritePermission(mActivity); - - String versionName = BuildConfig.VERSION_NAME; - mView.setVersionString(versionName); - - GameFileCacheManager.getGameFiles().observe(mActivity, (gameFiles) -> mView.showGames()); - - Observer refreshObserver = (isLoading) -> - { - mView.setRefreshing(GameFileCacheManager.isLoadingOrRescanning()); - }; - GameFileCacheManager.isLoading().observe(mActivity, refreshObserver); - GameFileCacheManager.isRescanning().observe(mActivity, refreshObserver); - } - - public void onDestroy() - { - } - - public void onFabClick() - { - new AfterDirectoryInitializationRunner().runWithLifecycle(mActivity, - mView::launchFileListActivity); - } - - public boolean handleOptionSelection(int itemId, ComponentActivity activity) - { - switch (itemId) - { - case R.id.menu_settings: - mView.launchSettingsActivity(MenuTag.SETTINGS); - return true; - - case R.id.menu_grid_options: - mView.showGridOptions(); - return true; - - case R.id.menu_refresh: - mView.setRefreshing(true); - GameFileCacheManager.startRescan(); - return true; - - case R.id.button_add_directory: - new AfterDirectoryInitializationRunner().runWithLifecycle(activity, - mView::launchFileListActivity); - return true; - - case R.id.menu_open_file: - mView.launchOpenFileActivity(REQUEST_GAME_FILE); - return true; - - case R.id.menu_load_wii_system_menu: - launchWiiSystemMenu(); - return true; - - case R.id.menu_online_system_update: - new AfterDirectoryInitializationRunner().runWithLifecycle(activity, - this::launchOnlineUpdate); - return true; - - case R.id.menu_install_wad: - new AfterDirectoryInitializationRunner().runWithLifecycle(activity, - () -> mView.launchOpenFileActivity(REQUEST_WAD_FILE)); - return true; - - case R.id.menu_import_wii_save: - new AfterDirectoryInitializationRunner().runWithLifecycle(activity, - () -> mView.launchOpenFileActivity(REQUEST_WII_SAVE_FILE)); - return true; - - case R.id.menu_import_nand_backup: - new AfterDirectoryInitializationRunner().runWithLifecycle(activity, - () -> mView.launchOpenFileActivity(REQUEST_NAND_BIN_FILE)); - return true; - - case R.id.menu_about: - showAboutDialog(); - } - - return false; - } - - public void onResume() - { - if (mDirToAdd != null) - { - GameFileCache.addGameFolder(mDirToAdd); - mDirToAdd = null; - } - - if (sShouldRescanLibrary) - { - GameFileCacheManager.startRescan(); - } - - sShouldRescanLibrary = true; - } - - /** - * Called when a selection is made using the legacy folder picker. - */ - public void onDirectorySelected(String dir) - { - mDirToAdd = dir; - } - - /** - * Called when a selection is made using the Storage Access Framework folder picker. - */ - public void onDirectorySelected(Intent result) - { - Uri uri = result.getData(); - - boolean recursive = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.getBoolean(); - String[] childNames = ContentHandler.getChildNames(uri, recursive); - if (Arrays.stream(childNames).noneMatch((name) -> FileBrowserHelper.GAME_EXTENSIONS.contains( - FileBrowserHelper.getExtension(name, false)))) - { - new MaterialAlertDialogBuilder(mActivity) - .setMessage(mActivity.getString(R.string.wrong_file_extension_in_directory, - FileBrowserHelper.setToSortedDelimitedString( - FileBrowserHelper.GAME_EXTENSIONS))) - .setPositiveButton(R.string.ok, null) - .show(); - } - - ContentResolver contentResolver = mActivity.getContentResolver(); - Uri canonicalizedUri = contentResolver.canonicalize(uri); - if (canonicalizedUri != null) - uri = canonicalizedUri; - - int takeFlags = result.getFlags() & Intent.FLAG_GRANT_READ_URI_PERMISSION; - mActivity.getContentResolver().takePersistableUriPermission(uri, takeFlags); - - mDirToAdd = uri.toString(); - } - - public void installWAD(String path) - { - ThreadUtil.runOnThreadAndShowResult(mActivity, R.string.import_in_progress, 0, () -> - { - boolean success = WiiUtils.installWAD(path); - int message = success ? R.string.wad_install_success : R.string.wad_install_failure; - return mActivity.getResources().getString(message); - }); - } - - public void importWiiSave(String path) - { - CompletableFuture canOverwriteFuture = new CompletableFuture<>(); - - ThreadUtil.runOnThreadAndShowResult(mActivity, R.string.import_in_progress, 0, () -> - { - BooleanSupplier canOverwrite = () -> - { - mActivity.runOnUiThread(() -> - { - new MaterialAlertDialogBuilder(mActivity) - .setMessage(R.string.wii_save_exists) - .setCancelable(false) - .setPositiveButton(R.string.yes, (dialog, i) -> canOverwriteFuture.complete(true)) - .setNegativeButton(R.string.no, (dialog, i) -> canOverwriteFuture.complete(false)) - .show(); - }); - - try - { - return canOverwriteFuture.get(); - } - catch (ExecutionException | InterruptedException e) - { - // Shouldn't happen - throw new RuntimeException(e); - } - }; - - int result = WiiUtils.importWiiSave(path, canOverwrite); - - int message; - switch (result) - { - case WiiUtils.RESULT_SUCCESS: - message = R.string.wii_save_import_success; - break; - case WiiUtils.RESULT_CORRUPTED_SOURCE: - message = R.string.wii_save_import_corruped_source; - break; - case WiiUtils.RESULT_TITLE_MISSING: - message = R.string.wii_save_import_title_missing; - break; - case WiiUtils.RESULT_CANCELLED: - return null; - default: - message = R.string.wii_save_import_error; - break; - } - return mActivity.getResources().getString(message); - }); - } - - public void importNANDBin(String path) - { - new MaterialAlertDialogBuilder(mActivity) - .setMessage(R.string.nand_import_warning) - .setNegativeButton(R.string.no, (dialog, i) -> dialog.dismiss()) - .setPositiveButton(R.string.yes, (dialog, i) -> - { - dialog.dismiss(); - - ThreadUtil.runOnThreadAndShowResult(mActivity, R.string.import_in_progress, - R.string.do_not_close_app, () -> - { - // ImportNANDBin unfortunately doesn't provide any result value... - // It does however show a panic alert if something goes wrong. - WiiUtils.importNANDBin(path); - return null; - }); - }) - .show(); - } - - public static void skipRescanningLibrary() - { - sShouldRescanLibrary = false; - } - - private void launchOnlineUpdate() - { - if (WiiUtils.isSystemMenuInstalled()) - { - SystemUpdateViewModel viewModel = - new ViewModelProvider(mActivity).get(SystemUpdateViewModel.class); - viewModel.setRegion(-1); - launchUpdateProgressBarFragment(mActivity); - } - else - { - SystemMenuNotInstalledDialogFragment dialogFragment = - new SystemMenuNotInstalledDialogFragment(); - dialogFragment - .show(mActivity.getSupportFragmentManager(), "SystemMenuNotInstalledDialogFragment"); - } - } - - public static void launchDiscUpdate(String path, FragmentActivity activity) - { - SystemUpdateViewModel viewModel = - new ViewModelProvider(activity).get(SystemUpdateViewModel.class); - viewModel.setDiscPath(path); - launchUpdateProgressBarFragment(activity); - } - - private static void launchUpdateProgressBarFragment(FragmentActivity activity) - { - SystemUpdateProgressBarDialogFragment progressBarFragment = - new SystemUpdateProgressBarDialogFragment(); - progressBarFragment - .show(activity.getSupportFragmentManager(), SystemUpdateProgressBarDialogFragment.TAG); - progressBarFragment.setCancelable(false); - } - - private void launchWiiSystemMenu() - { - new AfterDirectoryInitializationRunner().runWithLifecycle(mActivity, () -> - { - if (WiiUtils.isSystemMenuInstalled()) - { - EmulationActivity.launchSystemMenu(mActivity); - } - else - { - SystemMenuNotInstalledDialogFragment dialogFragment = - new SystemMenuNotInstalledDialogFragment(); - dialogFragment - .show(mActivity.getSupportFragmentManager(), - "SystemMenuNotInstalledDialogFragment"); - } - }); - } - - private void showAboutDialog() - { - new AboutDialogFragment().show(mActivity.getSupportFragmentManager(), AboutDialogFragment.TAG); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.kt new file mode 100644 index 0000000000..ddd2bcd2d0 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.kt @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.main + +import android.content.DialogInterface +import android.content.Intent +import androidx.activity.ComponentActivity +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.dolphinemu.dolphinemu.BuildConfig +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting +import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag +import org.dolphinemu.dolphinemu.features.sysupdate.ui.SystemMenuNotInstalledDialogFragment +import org.dolphinemu.dolphinemu.features.sysupdate.ui.SystemUpdateProgressBarDialogFragment +import org.dolphinemu.dolphinemu.features.sysupdate.ui.SystemUpdateViewModel +import org.dolphinemu.dolphinemu.fragments.AboutDialogFragment +import org.dolphinemu.dolphinemu.model.GameFileCache +import org.dolphinemu.dolphinemu.services.GameFileCacheManager +import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner +import org.dolphinemu.dolphinemu.utils.BooleanSupplier +import org.dolphinemu.dolphinemu.utils.CompletableFuture +import org.dolphinemu.dolphinemu.utils.ContentHandler +import org.dolphinemu.dolphinemu.utils.DirectoryInitialization +import org.dolphinemu.dolphinemu.utils.FileBrowserHelper +import org.dolphinemu.dolphinemu.utils.PermissionsHandler +import org.dolphinemu.dolphinemu.utils.ThreadUtil +import org.dolphinemu.dolphinemu.utils.WiiUtils +import java.util.Arrays +import java.util.concurrent.ExecutionException + +class MainPresenter(private val mainView: MainView, private val activity: FragmentActivity) { + private var dirToAdd: String? = null + + fun onCreate() { + // Ask the user to grant write permission if relevant and not already granted + if (DirectoryInitialization.isWaitingForWriteAccess(activity)) + PermissionsHandler.requestWritePermission(activity) + + val versionName = BuildConfig.VERSION_NAME + mainView.setVersionString(versionName) + + GameFileCacheManager.getGameFiles().observe(activity) { mainView.showGames() } + val refreshObserver = + Observer { _: Boolean? -> mainView.setRefreshing(GameFileCacheManager.isLoadingOrRescanning()) } + GameFileCacheManager.isLoading().observe(activity, refreshObserver) + GameFileCacheManager.isRescanning().observe(activity, refreshObserver) + } + + fun onFabClick() { + AfterDirectoryInitializationRunner().runWithLifecycle(activity) { mainView.launchFileListActivity() } + } + + fun handleOptionSelection(itemId: Int, activity: ComponentActivity): Boolean = + when (itemId) { + R.id.menu_settings -> { + mainView.launchSettingsActivity(MenuTag.SETTINGS) + true + } + + R.id.menu_grid_options -> { + mainView.showGridOptions() + true + } + + R.id.menu_refresh -> { + mainView.setRefreshing(true) + GameFileCacheManager.startRescan() + true + } + + R.id.button_add_directory -> { + AfterDirectoryInitializationRunner().runWithLifecycle(activity) { mainView.launchFileListActivity() } + true + } + + R.id.menu_open_file -> { + mainView.launchOpenFileActivity(REQUEST_GAME_FILE) + true + } + + R.id.menu_load_wii_system_menu -> { + launchWiiSystemMenu() + true + } + + R.id.menu_online_system_update -> { + AfterDirectoryInitializationRunner().runWithLifecycle(activity) { launchOnlineUpdate() } + true + } + + R.id.menu_install_wad -> { + AfterDirectoryInitializationRunner().runWithLifecycle( + activity + ) { mainView.launchOpenFileActivity(REQUEST_WAD_FILE) } + true + } + + R.id.menu_import_wii_save -> { + AfterDirectoryInitializationRunner().runWithLifecycle( + activity + ) { mainView.launchOpenFileActivity(REQUEST_WII_SAVE_FILE) } + true + } + + R.id.menu_import_nand_backup -> { + AfterDirectoryInitializationRunner().runWithLifecycle( + activity + ) { mainView.launchOpenFileActivity(REQUEST_NAND_BIN_FILE) } + true + } + + R.id.menu_about -> { + showAboutDialog() + false + } + + else -> false + } + + fun onResume() { + if (dirToAdd != null) { + GameFileCache.addGameFolder(dirToAdd) + dirToAdd = null + } + + if (shouldRescanLibrary) { + GameFileCacheManager.startRescan() + } + + shouldRescanLibrary = true + } + + /** + * Called when a selection is made using the legacy folder picker. + */ + fun onDirectorySelected(dir: String?) { + dirToAdd = dir + } + + /** + * Called when a selection is made using the Storage Access Framework folder picker. + */ + fun onDirectorySelected(result: Intent) { + var uri = result.data!! + + val recursive = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.boolean + val childNames = ContentHandler.getChildNames(uri, recursive) + if (Arrays.stream(childNames).noneMatch { + FileBrowserHelper.GAME_EXTENSIONS + .contains(FileBrowserHelper.getExtension(it, false)) + }) { + MaterialAlertDialogBuilder(activity) + .setMessage( + activity.getString( + R.string.wrong_file_extension_in_directory, + FileBrowserHelper.setToSortedDelimitedString(FileBrowserHelper.GAME_EXTENSIONS) + ) + ) + .setPositiveButton(android.R.string.ok, null) + .show() + } + + val contentResolver = activity.contentResolver + val canonicalizedUri = contentResolver.canonicalize(uri) + if (canonicalizedUri != null) + uri = canonicalizedUri + + val takeFlags = result.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION + activity.contentResolver.takePersistableUriPermission(uri, takeFlags) + + dirToAdd = uri.toString() + } + + fun installWAD(path: String?) { + ThreadUtil.runOnThreadAndShowResult( + activity, + R.string.import_in_progress, + 0, + { + val success = WiiUtils.installWAD(path!!) + val message = + if (success) R.string.wad_install_success else R.string.wad_install_failure + activity.getString(message) + }) + } + + fun importWiiSave(path: String?) { + val canOverwriteFuture = CompletableFuture() + ThreadUtil.runOnThreadAndShowResult( + activity, + R.string.import_in_progress, + 0, + { + val canOverwrite = BooleanSupplier { + activity.runOnUiThread { + MaterialAlertDialogBuilder(activity) + .setMessage(R.string.wii_save_exists) + .setCancelable(false) + .setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> + canOverwriteFuture.complete(true) + } + .setNegativeButton(R.string.no) { _: DialogInterface?, _: Int -> + canOverwriteFuture.complete(false) + } + .show() + } + try { + return@BooleanSupplier canOverwriteFuture.get() + } catch (e: ExecutionException) { + // Shouldn't happen + throw RuntimeException(e) + } catch (e: InterruptedException) { + throw RuntimeException(e) + } + } + + val message: Int = when (WiiUtils.importWiiSave(path!!, canOverwrite)) { + WiiUtils.RESULT_SUCCESS -> R.string.wii_save_import_success + WiiUtils.RESULT_CORRUPTED_SOURCE -> R.string.wii_save_import_corruped_source + WiiUtils.RESULT_TITLE_MISSING -> R.string.wii_save_import_title_missing + WiiUtils.RESULT_CANCELLED -> return@runOnThreadAndShowResult null + else -> R.string.wii_save_import_error + } + activity.resources.getString(message) + }) + } + + fun importNANDBin(path: String?) { + MaterialAlertDialogBuilder(activity) + .setMessage(R.string.nand_import_warning) + .setNegativeButton(R.string.no) { dialog: DialogInterface, _: Int -> dialog.dismiss() } + .setPositiveButton(R.string.yes) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + ThreadUtil.runOnThreadAndShowResult( + activity, + R.string.import_in_progress, + R.string.do_not_close_app, + { + // ImportNANDBin unfortunately doesn't provide any result value... + // It does however show a panic alert if something goes wrong. + WiiUtils.importNANDBin(path!!) + null + }) + } + .show() + } + + private fun launchOnlineUpdate() { + if (WiiUtils.isSystemMenuInstalled()) { + val viewModel = ViewModelProvider(activity)[SystemUpdateViewModel::class.java] + viewModel.region = -1 + launchUpdateProgressBarFragment(activity) + } else { + SystemMenuNotInstalledDialogFragment().show( + activity.supportFragmentManager, + SystemMenuNotInstalledDialogFragment.TAG + ) + } + } + + private fun launchWiiSystemMenu() { + AfterDirectoryInitializationRunner().runWithLifecycle(activity) { + if (WiiUtils.isSystemMenuInstalled()) { + EmulationActivity.launchSystemMenu(activity) + } else { + SystemMenuNotInstalledDialogFragment().show( + activity.supportFragmentManager, + SystemMenuNotInstalledDialogFragment.TAG + ) + } + } + } + + private fun showAboutDialog() { + AboutDialogFragment().show(activity.supportFragmentManager, AboutDialogFragment.TAG) + } + + companion object { + const val REQUEST_DIRECTORY = 1 + const val REQUEST_GAME_FILE = 2 + const val REQUEST_SD_FILE = 3 + const val REQUEST_WAD_FILE = 4 + const val REQUEST_WII_SAVE_FILE = 5 + const val REQUEST_NAND_BIN_FILE = 6 + + private var shouldRescanLibrary = true + + @JvmStatic + fun skipRescanningLibrary() { + shouldRescanLibrary = false + } + + @JvmStatic + fun launchDiscUpdate(path: String, activity: FragmentActivity) { + val viewModel = ViewModelProvider(activity)[SystemUpdateViewModel::class.java] + viewModel.discPath = path + launchUpdateProgressBarFragment(activity) + } + + private fun launchUpdateProgressBarFragment(activity: FragmentActivity) { + val progressBarFragment = SystemUpdateProgressBarDialogFragment() + progressBarFragment + .show(activity.supportFragmentManager, SystemUpdateProgressBarDialogFragment.TAG) + progressBarFragment.isCancelable = false + } + } +} From b5c63b995ce7d9a7c8eb131567c2759950a925a5 Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Sat, 3 Jun 2023 20:40:06 -0400 Subject: [PATCH 04/10] Android: Convert MainView to Kotlin --- .../dolphinemu/ui/main/MainView.java | 41 ------------------- .../dolphinemu/dolphinemu/ui/main/MainView.kt | 40 ++++++++++++++++++ 2 files changed, 40 insertions(+), 41 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.java deleted file mode 100644 index 604cb5d1a8..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.java +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.ui.main; - -import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag; - -/** - * Abstraction for the screen that shows on application launch. - * Implementations will differ primarily to target touch-screen - * or non-touch screen devices. - */ -public interface MainView -{ - /** - * Pass the view the native library's version string. Displaying - * it is optional. - * - * @param version A string pulled from native code. - */ - void setVersionString(String version); - - void launchSettingsActivity(MenuTag menuTag); - - void launchFileListActivity(); - - void launchOpenFileActivity(int requestCode); - - /** - * Shows or hides the loading indicator. - */ - void setRefreshing(boolean refreshing); - - /** - * To be called when the game file cache is updated. - */ - void showGames(); - - void reloadGrid(); - - void showGridOptions(); -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.kt new file mode 100644 index 0000000000..3892ee7599 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.kt @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.main + +import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag + +/** + * Abstraction for the screen that shows on application launch. + * Implementations will differ primarily to target touch-screen + * or non-touch screen devices. + */ +interface MainView { + /** + * Pass the view the native library's version string. Displaying + * it is optional. + * + * @param version A string pulled from native code. + */ + fun setVersionString(version: String) + + fun launchSettingsActivity(menuTag: MenuTag?) + + fun launchFileListActivity() + + fun launchOpenFileActivity(requestCode: Int) + + /** + * Shows or hides the loading indicator. + */ + fun setRefreshing(refreshing: Boolean) + + /** + * To be called when the game file cache is updated. + */ + fun showGames() + + fun reloadGrid() + + fun showGridOptions() +} From ecd62b221113edcc790fcf05155c6c49087c35bd Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Sat, 3 Jun 2023 20:40:26 -0400 Subject: [PATCH 05/10] Android: Convert ThemeProvider to Kotlin --- .../dolphinemu/dolphinemu/ui/main/ThemeProvider.java | 9 --------- .../org/dolphinemu/dolphinemu/ui/main/ThemeProvider.kt | 10 ++++++++++ 2 files changed, 10 insertions(+), 9 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/ThemeProvider.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/ThemeProvider.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/ThemeProvider.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/ThemeProvider.java deleted file mode 100644 index 1be137f33e..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/ThemeProvider.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.dolphinemu.dolphinemu.ui.main; - -public interface ThemeProvider -{ - /** - * Provides theme ID by overriding an activity's 'setTheme' method and returning that result - */ - int getThemeId(); -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/ThemeProvider.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/ThemeProvider.kt new file mode 100644 index 0000000000..b49011a987 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/ThemeProvider.kt @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.main + +interface ThemeProvider { + /** + * Provides theme ID by overriding an activity's 'setTheme' method and returning that result + */ + val themeId: Int +} From 2434c2db5975db2dec53541ced7184144e15f03f Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Sat, 3 Jun 2023 20:40:38 -0400 Subject: [PATCH 06/10] Android: Convert TvMainActivity to Kotlin --- .../dolphinemu/ui/main/TvMainActivity.java | 418 ------------------ .../dolphinemu/ui/main/TvMainActivity.kt | 370 ++++++++++++++++ 2 files changed, 370 insertions(+), 418 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java deleted file mode 100644 index 2da1b8591d..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java +++ /dev/null @@ -1,418 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.ui.main; - -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; -import androidx.core.splashscreen.SplashScreen; -import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentManager; -import androidx.leanback.app.BrowseSupportFragment; -import androidx.leanback.widget.ArrayObjectAdapter; -import androidx.leanback.widget.HeaderItem; -import androidx.leanback.widget.ListRow; -import androidx.leanback.widget.ListRowPresenter; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import org.dolphinemu.dolphinemu.fragments.GridOptionDialogFragment; -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.activities.EmulationActivity; -import org.dolphinemu.dolphinemu.adapters.GameRowPresenter; -import org.dolphinemu.dolphinemu.adapters.SettingsRowPresenter; -import org.dolphinemu.dolphinemu.databinding.ActivityTvMainBinding; -import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag; -import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity; -import org.dolphinemu.dolphinemu.model.GameFile; -import org.dolphinemu.dolphinemu.model.TvSettingsItem; -import org.dolphinemu.dolphinemu.services.GameFileCacheManager; -import org.dolphinemu.dolphinemu.ui.platform.Platform; -import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; -import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; -import org.dolphinemu.dolphinemu.utils.PermissionsHandler; -import org.dolphinemu.dolphinemu.utils.StartupHandler; -import org.dolphinemu.dolphinemu.utils.TvUtil; -import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder; - -import java.util.ArrayList; -import java.util.Collection; - -public final class TvMainActivity extends FragmentActivity - implements MainView, SwipeRefreshLayout.OnRefreshListener -{ - private final MainPresenter mPresenter = new MainPresenter(this, this); - - private SwipeRefreshLayout mSwipeRefresh; - - private BrowseSupportFragment mBrowseFragment; - - private final ArrayList mGameRows = new ArrayList<>(); - - private ActivityTvMainBinding mBinding; - - @Override - protected void onCreate(Bundle savedInstanceState) - { - SplashScreen splashScreen = SplashScreen.installSplashScreen(this); - splashScreen.setKeepOnScreenCondition( - () -> !DirectoryInitialization.areDolphinDirectoriesReady()); - - super.onCreate(savedInstanceState); - mBinding = ActivityTvMainBinding.inflate(getLayoutInflater()); - setContentView(mBinding.getRoot()); - - setupUI(); - - mPresenter.onCreate(); - - // Stuff in this block only happens when this activity is newly created (i.e. not a rotation) - if (savedInstanceState == null) - { - StartupHandler.HandleInit(this); - } - } - - @Override - protected void onResume() - { - super.onResume(); - - if (DirectoryInitialization.shouldStart(this)) - { - DirectoryInitialization.start(this); - GameFileCacheManager.startLoad(); - } - - mPresenter.onResume(); - } - - @Override - protected void onDestroy() - { - super.onDestroy(); - mPresenter.onDestroy(); - } - - @Override - protected void onStart() - { - super.onStart(); - StartupHandler.checkSessionReset(this); - } - - @Override - protected void onStop() - { - super.onStop(); - - if (isChangingConfigurations()) - { - MainPresenter.skipRescanningLibrary(); - } - - StartupHandler.setSessionTime(this); - } - - void setupUI() - { - mSwipeRefresh = mBinding.swipeRefresh; - - mSwipeRefresh.setOnRefreshListener(this); - - setRefreshing(GameFileCacheManager.isLoadingOrRescanning()); - - final FragmentManager fragmentManager = getSupportFragmentManager(); - mBrowseFragment = new BrowseSupportFragment(); - fragmentManager - .beginTransaction() - .add(R.id.content, mBrowseFragment, "BrowseFragment") - .commit(); - - // Set display parameters for the BrowseFragment - mBrowseFragment.setHeadersState(BrowseSupportFragment.HEADERS_ENABLED); - mBrowseFragment.setBrandColor(ContextCompat.getColor(this, R.color.dolphin_blue)); - buildRowsAdapter(); - - mBrowseFragment.setOnItemViewClickedListener( - (itemViewHolder, item, rowViewHolder, row) -> - { - // Special case: user clicked on a settings row item. - if (item instanceof TvSettingsItem) - { - TvSettingsItem settingsItem = (TvSettingsItem) item; - mPresenter.handleOptionSelection(settingsItem.getItemId(), this); - } - else - { - TvGameViewHolder holder = (TvGameViewHolder) itemViewHolder; - - // Start the emulation activity and send the path of the clicked ISO to it. - String[] paths = GameFileCacheManager.findSecondDiscAndGetPaths(holder.gameFile); - EmulationActivity.launch(TvMainActivity.this, paths, false); - } - }); - } - - /** - * MainView - */ - - @Override - public void setVersionString(String version) - { - mBrowseFragment.setTitle(version); - } - - @Override - public void launchSettingsActivity(MenuTag menuTag) - { - SettingsActivity.launch(this, menuTag); - } - - @Override - public void launchFileListActivity() - { - if (DirectoryInitialization.preferOldFolderPicker(this)) - { - FileBrowserHelper.openDirectoryPicker(this, FileBrowserHelper.GAME_EXTENSIONS); - } - else - { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - startActivityForResult(intent, MainPresenter.REQUEST_DIRECTORY); - } - } - - @Override - public void launchOpenFileActivity(int requestCode) - { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); - startActivityForResult(intent, requestCode); - } - - /** - * Shows or hides the loading indicator. - */ - @Override - public void setRefreshing(boolean refreshing) - { - mSwipeRefresh.setRefreshing(refreshing); - } - - @Override - public void showGames() - { - // Kicks off the program services to update all channels - TvUtil.updateAllChannels(getApplicationContext()); - - buildRowsAdapter(); - } - - @Override - public void reloadGrid() - { - for (ArrayObjectAdapter row : mGameRows) - { - row.notifyArrayItemRangeChanged(0, row.size()); - } - } - - @Override - public void showGridOptions() - { - new GridOptionDialogFragment().show(getSupportFragmentManager(), "gridOptions"); - } - - /** - * Callback from AddDirectoryActivity. Applies any changes necessary to the GameGridActivity. - * - * @param requestCode An int describing whether the Activity that is returning did so successfully. - * @param resultCode An int describing what Activity is giving us this callback. - * @param result The information the returning Activity is providing us. - */ - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent result) - { - super.onActivityResult(requestCode, resultCode, result); - - // If the user picked a file, as opposed to just backing out. - if (resultCode == RESULT_OK) - { - Uri uri = result.getData(); - switch (requestCode) - { - case MainPresenter.REQUEST_DIRECTORY: - if (DirectoryInitialization.preferOldFolderPicker(this)) - { - mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedPath(result)); - } - else - { - mPresenter.onDirectorySelected(result); - } - break; - - case MainPresenter.REQUEST_GAME_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, uri, - FileBrowserHelper.GAME_LIKE_EXTENSIONS, - () -> EmulationActivity.launch(this, result.getData().toString(), false)); - break; - - case MainPresenter.REQUEST_WAD_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.WAD_EXTENSION, - () -> mPresenter.installWAD(result.getData().toString())); - break; - - case MainPresenter.REQUEST_WII_SAVE_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.BIN_EXTENSION, - () -> mPresenter.importWiiSave(result.getData().toString())); - break; - - case MainPresenter.REQUEST_NAND_BIN_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.BIN_EXTENSION, - () -> mPresenter.importNANDBin(result.getData().toString())); - break; - } - } - else - { - MainPresenter.skipRescanningLibrary(); - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) - { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - if (requestCode == PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION) - { - if (grantResults[0] == PackageManager.PERMISSION_DENIED) - { - PermissionsHandler.setWritePermissionDenied(); - } - - DirectoryInitialization.start(this); - GameFileCacheManager.startLoad(); - } - } - - /** - * Called when the user requests a refresh by swiping down. - */ - @Override - public void onRefresh() - { - setRefreshing(true); - GameFileCacheManager.startRescan(); - } - - private void buildRowsAdapter() - { - ArrayObjectAdapter rowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); - mGameRows.clear(); - - if (!DirectoryInitialization.isWaitingForWriteAccess(this)) - { - GameFileCacheManager.startLoad(); - } - - for (Platform platform : Platform.values()) - { - ListRow row = buildGamesRow(platform, GameFileCacheManager.getGameFilesForPlatform(platform)); - - // Add row to the adapter only if it is not empty. - if (row != null) - { - rowsAdapter.add(row); - } - } - - rowsAdapter.add(buildSettingsRow()); - - mBrowseFragment.setAdapter(rowsAdapter); - } - - private ListRow buildGamesRow(Platform platform, Collection gameFiles) - { - // If there are no games, don't return a Row. - if (gameFiles.size() == 0) - { - return null; - } - - // Create an adapter for this row. - ArrayObjectAdapter row = new ArrayObjectAdapter(new GameRowPresenter(this)); - row.addAll(0, gameFiles); - - // Keep a reference to the row in case we need to refresh it. - mGameRows.add(row); - - // Create a header for this row. - HeaderItem header = new HeaderItem(platform.toInt(), getString(platform.getHeaderName())); - - // Create the row, passing it the filled adapter and the header, and give it to the master adapter. - return new ListRow(header, row); - } - - private ListRow buildSettingsRow() - { - ArrayObjectAdapter rowItems = new ArrayObjectAdapter(new SettingsRowPresenter()); - - rowItems.add(new TvSettingsItem(R.id.menu_settings, - R.drawable.ic_settings_tv, - R.string.grid_menu_settings)); - - rowItems.add(new TvSettingsItem(R.id.button_add_directory, - R.drawable.ic_add_tv, - R.string.add_directory_title)); - - rowItems.add(new TvSettingsItem(R.id.menu_grid_options, - R.drawable.ic_list_tv, - R.string.grid_menu_grid_options)); - - rowItems.add(new TvSettingsItem(R.id.menu_refresh, - R.drawable.ic_refresh_tv, - R.string.grid_menu_refresh)); - - rowItems.add(new TvSettingsItem(R.id.menu_open_file, - R.drawable.ic_play_tv, - R.string.grid_menu_open_file)); - - rowItems.add(new TvSettingsItem(R.id.menu_install_wad, - R.drawable.ic_folder_tv, - R.string.grid_menu_install_wad)); - - rowItems.add(new TvSettingsItem(R.id.menu_load_wii_system_menu, - R.drawable.ic_folder_tv, - R.string.grid_menu_load_wii_system_menu)); - - rowItems.add(new TvSettingsItem(R.id.menu_import_wii_save, - R.drawable.ic_folder_tv, - R.string.grid_menu_import_wii_save)); - - rowItems.add(new TvSettingsItem(R.id.menu_import_nand_backup, - R.drawable.ic_folder_tv, - R.string.grid_menu_import_nand_backup)); - - rowItems.add(new TvSettingsItem(R.id.menu_online_system_update, - R.drawable.ic_folder_tv, - R.string.grid_menu_online_system_update)); - - rowItems.add(new TvSettingsItem(R.id.menu_about, - R.drawable.ic_info_tv, - R.string.grid_menu_about)); - - // Create a header for this row. - HeaderItem header = new HeaderItem(R.string.settings, getString(R.string.settings)); - - return new ListRow(header, rowItems); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt new file mode 100644 index 0000000000..79ec2f2943 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.main + +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import androidx.core.content.ContextCompat +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.fragment.app.FragmentActivity +import androidx.leanback.app.BrowseSupportFragment +import androidx.leanback.widget.ArrayObjectAdapter +import androidx.leanback.widget.HeaderItem +import androidx.leanback.widget.ListRow +import androidx.leanback.widget.ListRowPresenter +import androidx.leanback.widget.OnItemViewClickedListener +import androidx.leanback.widget.Presenter +import androidx.leanback.widget.Row +import androidx.leanback.widget.RowPresenter +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.adapters.GameRowPresenter +import org.dolphinemu.dolphinemu.adapters.SettingsRowPresenter +import org.dolphinemu.dolphinemu.databinding.ActivityTvMainBinding +import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag +import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity +import org.dolphinemu.dolphinemu.fragments.GridOptionDialogFragment +import org.dolphinemu.dolphinemu.model.GameFile +import org.dolphinemu.dolphinemu.model.TvSettingsItem +import org.dolphinemu.dolphinemu.services.GameFileCacheManager +import org.dolphinemu.dolphinemu.ui.platform.Platform +import org.dolphinemu.dolphinemu.utils.DirectoryInitialization +import org.dolphinemu.dolphinemu.utils.FileBrowserHelper +import org.dolphinemu.dolphinemu.utils.PermissionsHandler +import org.dolphinemu.dolphinemu.utils.StartupHandler +import org.dolphinemu.dolphinemu.utils.TvUtil +import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder + +class TvMainActivity : FragmentActivity(), MainView, OnRefreshListener { + private val presenter = MainPresenter(this, this) + + private var swipeRefresh: SwipeRefreshLayout? = null + + private var browseFragment: BrowseSupportFragment? = null + + private val gameRows = ArrayList() + + private lateinit var binding: ActivityTvMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen().setKeepOnScreenCondition { !DirectoryInitialization.areDolphinDirectoriesReady() } + + super.onCreate(savedInstanceState) + binding = ActivityTvMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + setupUI() + + presenter.onCreate() + + // Stuff in this block only happens when this activity is newly created (i.e. not a rotation) + if (savedInstanceState == null) { + StartupHandler.HandleInit(this) + } + } + + override fun onResume() { + super.onResume() + if (DirectoryInitialization.shouldStart(this)) { + DirectoryInitialization.start(this) + GameFileCacheManager.startLoad() + } + + presenter.onResume() + } + + override fun onStart() { + super.onStart() + StartupHandler.checkSessionReset(this) + } + + override fun onStop() { + super.onStop() + if (isChangingConfigurations) { + MainPresenter.skipRescanningLibrary() + } + StartupHandler.setSessionTime(this) + } + + private fun setupUI() { + swipeRefresh = binding.swipeRefresh + swipeRefresh!!.setOnRefreshListener(this) + setRefreshing(GameFileCacheManager.isLoadingOrRescanning()) + + browseFragment = BrowseSupportFragment() + supportFragmentManager + .beginTransaction() + .add(R.id.content, browseFragment!!, "BrowseFragment") + .commit() + + // Set display parameters for the BrowseFragment + browseFragment?.headersState = BrowseSupportFragment.HEADERS_ENABLED + browseFragment?.brandColor = ContextCompat.getColor(this, R.color.dolphin_blue) + buildRowsAdapter() + + browseFragment?.onItemViewClickedListener = + OnItemViewClickedListener { itemViewHolder: Presenter.ViewHolder, item: Any?, _: RowPresenter.ViewHolder?, _: Row? -> + // Special case: user clicked on a settings row item. + if (item is TvSettingsItem) { + presenter.handleOptionSelection(item.itemId, this) + } else { + val holder = itemViewHolder as TvGameViewHolder + + // Start the emulation activity and send the path of the clicked ISO to it. + val paths = GameFileCacheManager.findSecondDiscAndGetPaths(holder.gameFile) + EmulationActivity.launch(this@TvMainActivity, paths, false) + } + } + } + + /** + * MainView + */ + override fun setVersionString(version: String) { + browseFragment?.title = version + } + + override fun launchSettingsActivity(menuTag: MenuTag?) { + SettingsActivity.launch(this, menuTag) + } + + override fun launchFileListActivity() { + if (DirectoryInitialization.preferOldFolderPicker(this)) { + FileBrowserHelper.openDirectoryPicker(this, FileBrowserHelper.GAME_EXTENSIONS) + } else { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + startActivityForResult(intent, MainPresenter.REQUEST_DIRECTORY) + } + } + + override fun launchOpenFileActivity(requestCode: Int) { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "*/*" + startActivityForResult(intent, requestCode) + } + + /** + * Shows or hides the loading indicator. + */ + override fun setRefreshing(refreshing: Boolean) { + swipeRefresh?.isRefreshing = refreshing + } + + override fun showGames() { + // Kicks off the program services to update all channels + TvUtil.updateAllChannels(applicationContext) + + buildRowsAdapter() + } + + override fun reloadGrid() { + for (row in gameRows) { + row.notifyArrayItemRangeChanged(0, row.size()) + } + } + + override fun showGridOptions() { + GridOptionDialogFragment().show(supportFragmentManager, "gridOptions") + } + + /** + * Callback from AddDirectoryActivity. Applies any changes necessary to the GameGridActivity. + * + * @param requestCode An int describing whether the Activity that is returning did so successfully. + * @param resultCode An int describing what Activity is giving us this callback. + * @param result The information the returning Activity is providing us. + */ + override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) { + super.onActivityResult(requestCode, resultCode, result) + + // If the user picked a file, as opposed to just backing out. + if (resultCode == RESULT_OK) { + val uri = result!!.data + when (requestCode) { + MainPresenter.REQUEST_DIRECTORY -> { + if (DirectoryInitialization.preferOldFolderPicker(this)) { + presenter.onDirectorySelected(FileBrowserHelper.getSelectedPath(result)) + } else { + presenter.onDirectorySelected(result) + } + } + + MainPresenter.REQUEST_GAME_FILE -> FileBrowserHelper.runAfterExtensionCheck( + this, uri, FileBrowserHelper.GAME_LIKE_EXTENSIONS + ) { EmulationActivity.launch(this, result.data.toString(), false) } + + MainPresenter.REQUEST_WAD_FILE -> FileBrowserHelper.runAfterExtensionCheck( + this, uri, FileBrowserHelper.WAD_EXTENSION + ) { presenter.installWAD(result.data.toString()) } + + MainPresenter.REQUEST_WII_SAVE_FILE -> FileBrowserHelper.runAfterExtensionCheck( + this, uri, FileBrowserHelper.BIN_EXTENSION + ) { presenter.importWiiSave(result.data.toString()) } + + MainPresenter.REQUEST_NAND_BIN_FILE -> FileBrowserHelper.runAfterExtensionCheck( + this, uri, FileBrowserHelper.BIN_EXTENSION + ) { presenter.importNANDBin(result.data.toString()) } + } + } else { + MainPresenter.skipRescanningLibrary() + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION) { + if (grantResults[0] == PackageManager.PERMISSION_DENIED) { + PermissionsHandler.setWritePermissionDenied() + } + + DirectoryInitialization.start(this) + GameFileCacheManager.startLoad() + } + } + + /** + * Called when the user requests a refresh by swiping down. + */ + override fun onRefresh() { + setRefreshing(true) + GameFileCacheManager.startRescan() + } + + private fun buildRowsAdapter() { + val rowsAdapter = ArrayObjectAdapter(ListRowPresenter()) + gameRows.clear() + + if (!DirectoryInitialization.isWaitingForWriteAccess(this)) { + GameFileCacheManager.startLoad() + } + + for (platform in Platform.values()) { + val row = + buildGamesRow(platform, GameFileCacheManager.getGameFilesForPlatform(platform)) + + // Add row to the adapter only if it is not empty. + if (row != null) { + rowsAdapter.add(row) + } + } + + rowsAdapter.add(buildSettingsRow()) + + browseFragment!!.adapter = rowsAdapter + } + + private fun buildGamesRow(platform: Platform, gameFiles: Collection): ListRow? { + // If there are no games, don't return a Row. + if (gameFiles.isEmpty()) { + return null + } + + // Create an adapter for this row. + val row = ArrayObjectAdapter(GameRowPresenter(this)) + row.addAll(0, gameFiles) + + // Keep a reference to the row in case we need to refresh it. + gameRows.add(row) + + // Create a header for this row. + val header = HeaderItem(platform.toInt().toLong(), getString(platform.headerName)) + + // Create the row, passing it the filled adapter and the header, and give it to the master adapter. + return ListRow(header, row) + } + + private fun buildSettingsRow(): ListRow { + val rowItems = ArrayObjectAdapter(SettingsRowPresenter()) + rowItems.apply { + add( + TvSettingsItem( + R.id.menu_settings, + R.drawable.ic_settings_tv, + R.string.grid_menu_settings + ) + ) + add( + TvSettingsItem( + R.id.button_add_directory, + R.drawable.ic_add_tv, + R.string.add_directory_title + ) + ) + add( + TvSettingsItem( + R.id.menu_grid_options, + R.drawable.ic_list_tv, + R.string.grid_menu_grid_options + ) + ) + add( + TvSettingsItem( + R.id.menu_refresh, + R.drawable.ic_refresh_tv, + R.string.grid_menu_refresh + ) + ) + add( + TvSettingsItem( + R.id.menu_open_file, + R.drawable.ic_play_tv, + R.string.grid_menu_open_file + ) + ) + add( + TvSettingsItem( + R.id.menu_install_wad, + R.drawable.ic_folder_tv, + R.string.grid_menu_install_wad + ) + ) + add( + TvSettingsItem( + R.id.menu_load_wii_system_menu, + R.drawable.ic_folder_tv, + R.string.grid_menu_load_wii_system_menu + ) + ) + add( + TvSettingsItem( + R.id.menu_import_wii_save, + R.drawable.ic_folder_tv, + R.string.grid_menu_import_wii_save + ) + ) + add( + TvSettingsItem( + R.id.menu_import_nand_backup, + R.drawable.ic_folder_tv, + R.string.grid_menu_import_nand_backup + ) + ) + add( + TvSettingsItem( + R.id.menu_online_system_update, + R.drawable.ic_folder_tv, + R.string.grid_menu_online_system_update + ) + ) + add( + TvSettingsItem( + R.id.menu_about, + R.drawable.ic_info_tv, + R.string.grid_menu_about + ) + ) + } + + // Create a header for this row. + val header = HeaderItem(R.string.settings.toLong(), getString(R.string.settings)) + return ListRow(header, rowItems) + } +} \ No newline at end of file From 90ac08c98d7ccc1207e46a818248b43541bf891b Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Sat, 3 Jun 2023 20:40:54 -0400 Subject: [PATCH 07/10] Android: Convert NVidiaShieldWorkaroundView to Kotlin --- .../ui/NVidiaShieldWorkaroundView.java | 27 ------------------- .../ui/NVidiaShieldWorkaroundView.kt | 21 +++++++++++++++ 2 files changed, 21 insertions(+), 27 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/NVidiaShieldWorkaroundView.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/NVidiaShieldWorkaroundView.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/NVidiaShieldWorkaroundView.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/NVidiaShieldWorkaroundView.java deleted file mode 100644 index 4b4b763c2b..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/NVidiaShieldWorkaroundView.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2013 Dolphin Emulator Project - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -package org.dolphinemu.dolphinemu.ui; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; - -/** - * Work around a bug with the nVidia Shield. - * - * Without this View, the emulation SurfaceView acts like it has the - * highest Z-value, blocking any other View, such as the menu fragments. - */ -public final class NVidiaShieldWorkaroundView extends View -{ - public NVidiaShieldWorkaroundView(Context context, AttributeSet attrs) - { - super(context, attrs); - - // Setting this seems to workaround the bug - setWillNotDraw(false); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/NVidiaShieldWorkaroundView.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/NVidiaShieldWorkaroundView.kt new file mode 100644 index 0000000000..16679e2c5f --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/NVidiaShieldWorkaroundView.kt @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui + +import android.content.Context +import android.util.AttributeSet +import android.view.View + +/** + * Work around a bug with the nVidia Shield. + * + * + * Without this View, the emulation SurfaceView acts like it has the + * highest Z-value, blocking any other View, such as the menu fragments. + */ +class NVidiaShieldWorkaroundView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { + init { + // Setting this seems to workaround the bug + setWillNotDraw(false) + } +} From 0915bfbb30124ad1ec2dc58f4a083e31696613fc Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Sat, 3 Jun 2023 20:41:06 -0400 Subject: [PATCH 08/10] Android: Convert Platform to Kotlin --- .../dolphinemu/ui/platform/Platform.java | 58 ------------------- .../dolphinemu/ui/platform/Platform.kt | 36 ++++++++++++ 2 files changed, 36 insertions(+), 58 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/Platform.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/Platform.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/Platform.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/Platform.java deleted file mode 100644 index f561feb250..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/Platform.java +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.ui.platform; - -import org.dolphinemu.dolphinemu.R; - -/** - * Enum to represent platform (eg GameCube, Wii). - */ -public enum Platform -{ - GAMECUBE(0, R.string.platform_gamecube, "GameCube Games"), - WII(1, R.string.platform_wii, "Wii Games"), - WIIWARE(2, R.string.platform_wiiware, "WiiWare Games"); - - private final int value; - private final int headerName; - private final String idString; - - Platform(int value, int headerName, String idString) - { - this.value = value; - this.headerName = headerName; - this.idString = idString; - } - - public static Platform fromInt(int i) - { - return values()[i]; - } - - public static Platform fromNativeInt(int i) - { - // TODO: Proper support for DOL and ELF files - boolean in_range = i >= 0 && i < values().length; - return values()[in_range ? i : WIIWARE.value]; - } - - public static Platform fromPosition(int position) - { - return values()[position]; - } - - public int toInt() - { - return value; - } - - public int getHeaderName() - { - return headerName; - } - - public String getIdString() - { - return idString; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/Platform.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/Platform.kt new file mode 100644 index 0000000000..0bf3e0ea68 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/Platform.kt @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.platform + +import org.dolphinemu.dolphinemu.R + +/** + * Enum to represent platform (eg GameCube, Wii). + */ +enum class Platform(private val value: Int, val headerName: Int, val idString: String) { + GAMECUBE(0, R.string.platform_gamecube, "GameCube Games"), + WII(1, R.string.platform_wii, "Wii Games"), + WIIWARE(2, R.string.platform_wiiware, "WiiWare Games"); + + fun toInt(): Int { + return value + } + + companion object { + fun fromInt(i: Int): Platform { + return values()[i] + } + + @JvmStatic + fun fromNativeInt(i: Int): Platform { + // TODO: Proper support for DOL and ELF files + val inRange = i >= 0 && i < values().size + return values()[if (inRange) i else WIIWARE.value] + } + + @JvmStatic + fun fromPosition(position: Int): Platform { + return values()[position] + } + } +} From 3e8d6b8aa2ef35e710a5bfcc90174250e643d4bf Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Sat, 3 Jun 2023 20:41:17 -0400 Subject: [PATCH 09/10] Android: Convert PlatformGamesFragment to Kotlin --- .../ui/platform/PlatformGamesFragment.java | 147 ------------------ .../ui/platform/PlatformGamesFragment.kt | 122 +++++++++++++++ 2 files changed, 122 insertions(+), 147 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.java deleted file mode 100644 index 9393a48baf..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.java +++ /dev/null @@ -1,147 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.ui.platform; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.graphics.Insets; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowInsetsCompat; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import com.google.android.material.color.MaterialColors; - -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.adapters.GameAdapter; -import org.dolphinemu.dolphinemu.databinding.FragmentGridBinding; -import org.dolphinemu.dolphinemu.layout.AutofitGridLayoutManager; -import org.dolphinemu.dolphinemu.services.GameFileCacheManager; - -public final class PlatformGamesFragment extends Fragment implements PlatformGamesView -{ - private static final String ARG_PLATFORM = "platform"; - - private SwipeRefreshLayout mSwipeRefresh; - private SwipeRefreshLayout.OnRefreshListener mOnRefreshListener; - - private FragmentGridBinding mBinding; - - public static PlatformGamesFragment newInstance(Platform platform) - { - PlatformGamesFragment fragment = new PlatformGamesFragment(); - - Bundle args = new Bundle(); - args.putSerializable(ARG_PLATFORM, platform); - - fragment.setArguments(args); - return fragment; - } - - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - } - - @NonNull - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) - { - mBinding = FragmentGridBinding.inflate(inflater, container, false); - return mBinding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull View view, Bundle savedInstanceState) - { - mSwipeRefresh = mBinding.swipeRefresh; - GameAdapter adapter = new GameAdapter(requireActivity()); - adapter.setStateRestorationPolicy( - RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY); - mBinding.gridGames.setAdapter(adapter); - mBinding.gridGames.setLayoutManager(new AutofitGridLayoutManager(requireContext(), - getResources().getDimensionPixelSize(R.dimen.card_width))); - - // Set theme color to the refresh animation's background - mSwipeRefresh.setProgressBackgroundColorSchemeColor( - MaterialColors.getColor(mSwipeRefresh, R.attr.colorPrimary)); - mSwipeRefresh.setColorSchemeColors( - MaterialColors.getColor(mSwipeRefresh, R.attr.colorOnPrimary)); - - mSwipeRefresh.setOnRefreshListener(mOnRefreshListener); - - setInsets(); - - setRefreshing(GameFileCacheManager.isLoadingOrRescanning()); - - showGames(); - } - - @Override - public void onDestroyView() - { - super.onDestroyView(); - mBinding = null; - } - - @Override - public void onItemClick(String gameId) - { - // No-op for now - } - - @Override - public void showGames() - { - if (mBinding == null) - return; - - if (mBinding.gridGames.getAdapter() != null) - { - Platform platform = (Platform) getArguments().getSerializable(ARG_PLATFORM); - ((GameAdapter) mBinding.gridGames.getAdapter()).swapDataSet( - GameFileCacheManager.getGameFilesForPlatform(platform)); - } - } - - @Override - public void refetchMetadata() - { - ((GameAdapter) mBinding.gridGames.getAdapter()).refetchMetadata(); - } - - public void setOnRefreshListener(@Nullable SwipeRefreshLayout.OnRefreshListener listener) - { - mOnRefreshListener = listener; - - if (mSwipeRefresh != null) - { - mSwipeRefresh.setOnRefreshListener(listener); - } - } - - public void setRefreshing(boolean refreshing) - { - mBinding.swipeRefresh.setRefreshing(refreshing); - } - - private void setInsets() - { - ViewCompat.setOnApplyWindowInsetsListener(mBinding.gridGames, (v, windowInsets) -> - { - Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); - v.setPadding(0, 0, 0, - insets.bottom + getResources().getDimensionPixelSize(R.dimen.spacing_list) + - getResources().getDimensionPixelSize(R.dimen.spacing_fab)); - return windowInsets; - }); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.kt new file mode 100644 index 0000000000..5f1139da8a --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.kt @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.platform + +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.fragment.app.Fragment +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import com.google.android.material.color.MaterialColors +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.adapters.GameAdapter +import org.dolphinemu.dolphinemu.databinding.FragmentGridBinding +import org.dolphinemu.dolphinemu.layout.AutofitGridLayoutManager +import org.dolphinemu.dolphinemu.services.GameFileCacheManager + +class PlatformGamesFragment : Fragment(), PlatformGamesView { + private var swipeRefresh: SwipeRefreshLayout? = null + private var onRefreshListener: OnRefreshListener? = null + + private var _binding: FragmentGridBinding? = null + private val binding: FragmentGridBinding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentGridBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + swipeRefresh = binding.swipeRefresh + val gameAdapter = GameAdapter(requireActivity()) + gameAdapter.stateRestorationPolicy = + RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY + + binding.gridGames.apply { + adapter = gameAdapter + layoutManager = AutofitGridLayoutManager( + requireContext(), + resources.getDimensionPixelSize(R.dimen.card_width) + ) + } + + // Set theme color to the refresh animation's background + binding.swipeRefresh.apply { + setProgressBackgroundColorSchemeColor( + MaterialColors.getColor(swipeRefresh!!, R.attr.colorPrimary) + ) + setColorSchemeColors(MaterialColors.getColor(swipeRefresh!!, R.attr.colorOnPrimary)) + setOnRefreshListener(onRefreshListener) + } + + setInsets() + setRefreshing(GameFileCacheManager.isLoadingOrRescanning()) + showGames() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun showGames() { + if (_binding == null) + return + + if (binding.gridGames.adapter != null) { + val platform = requireArguments().getSerializable(ARG_PLATFORM) as Platform + (binding.gridGames.adapter as GameAdapter?)!!.swapDataSet( + GameFileCacheManager.getGameFilesForPlatform(platform) + ) + } + } + + override fun refetchMetadata() { + (binding.gridGames.adapter as GameAdapter).refetchMetadata() + } + + fun setOnRefreshListener(listener: OnRefreshListener?) { + onRefreshListener = listener + swipeRefresh?.setOnRefreshListener(listener) + } + + override fun setRefreshing(refreshing: Boolean) { + binding.swipeRefresh.isRefreshing = refreshing + } + + private fun setInsets() { + ViewCompat.setOnApplyWindowInsetsListener(binding.gridGames) { v: View, windowInsets: WindowInsetsCompat -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding( + 0, + 0, + 0, + insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_list) + + resources.getDimensionPixelSize(R.dimen.spacing_fab) + ) + windowInsets + } + } + + companion object { + private const val ARG_PLATFORM = "platform" + + @JvmStatic + fun newInstance(platform: Platform?): PlatformGamesFragment { + val fragment = PlatformGamesFragment() + val args = Bundle() + args.putSerializable(ARG_PLATFORM, platform) + fragment.arguments = args + return fragment + } + } +} From f117e8a2f96ee5e5578407c38697cc20ed91a3cd Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Sat, 3 Jun 2023 20:41:29 -0400 Subject: [PATCH 10/10] Android: Convert PlatformGamesView to Kotlin --- .../ui/platform/PlatformGamesView.java | 32 ------------------- .../ui/platform/PlatformGamesView.kt | 31 ++++++++++++++++++ 2 files changed, 31 insertions(+), 32 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesView.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesView.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesView.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesView.java deleted file mode 100644 index 642c531946..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesView.java +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.ui.platform; - -/** - * Abstraction for a screen representing a single platform's games. - */ -public interface PlatformGamesView -{ - /** - * Pass a click event to the view's Presenter. Typically called from the - * view's list adapter. - * - * @param gameId The ID of the game that was clicked. - */ - void onItemClick(String gameId); - - /** - * Shows or hides the loading indicator. - */ - void setRefreshing(boolean refreshing); - - /** - * To be called when the game file cache is updated. - */ - void showGames(); - - /** - * Re-fetches game metadata from the game file cache. - */ - void refetchMetadata(); -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesView.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesView.kt new file mode 100644 index 0000000000..b9d188c2e9 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesView.kt @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.ui.platform + +/** + * Abstraction for a screen representing a single platform's games. + */ +interface PlatformGamesView { + /** + * Pass a click event to the view's Presenter. Typically called from the + * view's list adapter. + * + * @param gameId The ID of the game that was clicked. + */ + fun onItemClick(gameId: String) { /* Empty default impl */ } + + /** + * Shows or hides the loading indicator. + */ + fun setRefreshing(refreshing: Boolean) + + /** + * To be called when the game file cache is updated. + */ + fun showGames() + + /** + * Re-fetches game metadata from the game file cache. + */ + fun refetchMetadata() +}