Merge pull request #11886 from t895/kotlin-ui

Android: Convert "ui" package to Kotlin
This commit is contained in:
JosJuice 2023-06-06 13:03:02 +02:00 committed by GitHub
commit 36ca747d55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1321 additions and 1607 deletions

View File

@ -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);
}
}

View File

@ -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)
}
}

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -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<PlatformGamesView> 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;
});
}
}

View File

@ -0,0 +1,327 @@
// 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 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<String>,
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<PlatformGamesView>) {
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
}
}

View File

@ -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<Boolean> 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<Boolean> 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);
}
}

View File

@ -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> { _: 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<Boolean>()
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
}
}
}

View File

@ -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();
}

View File

@ -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()
}

View File

@ -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();
}

View File

@ -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
}

View File

@ -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<ArrayObjectAdapter> 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<GameFile> 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);
}
}

View File

@ -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<ArrayObjectAdapter>()
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<String>,
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<GameFile?>): 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)
}
}

View File

@ -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;
}
}

View File

@ -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]
}
}
}

View File

@ -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;
});
}
}

View File

@ -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
}
}
}

View File

@ -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();
}

View File

@ -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()
}