diff options
Diffstat (limited to 'Source/Android/app/src/main')
12 files changed, 168 insertions, 10 deletions
diff --git a/Source/Android/app/src/main/AndroidManifest.xml b/Source/Android/app/src/main/AndroidManifest.xml index 96876acd46..5ff6ff090d 100644 --- a/Source/Android/app/src/main/AndroidManifest.xml +++ b/Source/Android/app/src/main/AndroidManifest.xml @@ -26,6 +26,7 @@ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.INTERNET"/> + <uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK"/> <uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA"/> <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA"/> <uses-permission android:name="android.permission.VIBRATE"/> diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt index b52b7b4fa4..e0ada502cc 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt @@ -367,6 +367,9 @@ object NativeLibrary { external fun PauseEmulation(overrideAchievementRestrictions: Boolean) @JvmStatic + external fun ResetEmulation() + + @JvmStatic external fun StopEmulation() /** diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt index 23ffe96cd6..2761c4b4e9 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt @@ -525,6 +525,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { MENU_ACTION_SKYLANDERS -> showSkylanderPortalSettings() MENU_ACTION_INFINITY_BASE -> showInfinityBaseSettings() MENU_ACTION_EXIT -> emulationFragment!!.stopEmulation() + MENU_ACTION_RESET -> emulationFragment!!.resetEmulation() } } @@ -1084,6 +1085,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { const val MENU_ACTION_SKYLANDERS = 36 const val MENU_ACTION_INFINITY_BASE = 37 const val MENU_ACTION_LATCHING_CONTROLS = 38 + const val MENU_ACTION_RESET = 39; init { buttonsActionsMap.apply { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/SettingsRowPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/SettingsRowPresenter.kt index f67a23e2da..eabfb75f82 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/SettingsRowPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/SettingsRowPresenter.kt @@ -3,6 +3,7 @@ package org.dolphinemu.dolphinemu.adapters import android.view.ViewGroup +import android.widget.TextView import androidx.core.content.ContextCompat import androidx.leanback.widget.ImageCardView import androidx.leanback.widget.Presenter @@ -16,9 +17,16 @@ class SettingsRowPresenter : Presenter() { val settingsCard = ImageCardView(parent.context) settingsCard.apply { setMainImageAdjustViewBounds(true) - setMainImageDimensions(192, 160) + setMainImageDimensions( + resources.getDimensionPixelSize(R.dimen.tv_settings_card_width), + resources.getDimensionPixelSize(R.dimen.tv_settings_card_height) + ) isFocusable = true isFocusableInTouchMode = true + findViewById<TextView>(R.id.title_text).apply { + minLines = 2 + maxLines = 2 + } } // Use that view to create a ViewHolder. diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt index ae222e8722..8b532b22de 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupScreen.kt @@ -1,9 +1,14 @@ // SPDX-License-Identifier: GPL-2.0-or-later -@file:OptIn(ExperimentalMaterial3Api::class) +@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) package org.dolphinemu.dolphinemu.features.netplay.ui +import android.Manifest +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.Settings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -19,6 +24,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.AlertDialog import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator @@ -42,13 +48,22 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.permissions.shouldShowRationale import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow @@ -58,6 +73,7 @@ import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType import org.dolphinemu.dolphinemu.ui.theme.DolphinScaffold import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer +import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox private data class ErrorDialogState(val message: String) { val onDismissed = CompletableDeferred<Unit>() @@ -87,6 +103,23 @@ fun NetplaySetupScreen( onHostClicked: () -> Unit, onConnectClicked: () -> Unit, ) { + val localNetworkPermissionState: PermissionState? = + if ( + connectionType is ConnectionType.DirectConnection && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN + ) { + rememberPermissionState(Manifest.permission.ACCESS_LOCAL_NETWORK) { _ -> + // Try to connect even if the user denied the permission. The warning is on screen + // so it's up to them to determine if they are using local network and need to grant it. + when (connectionRole) { + ConnectionRole.Host -> onHostClicked() + ConnectionRole.Connect -> onConnectClicked() + } + } + } else { + null + } + DolphinScaffold( title = { Text(stringResource(R.string.netplay_setup_title)) }, navigationIcon = { @@ -99,9 +132,19 @@ fun NetplaySetupScreen( }, floatingActionButton = { ExtendedFloatingActionButton( - onClick = when(connectionRole) { - ConnectionRole.Host -> onHostClicked - ConnectionRole.Connect -> onConnectClicked + onClick = { + if ( + localNetworkPermissionState != null && + !localNetworkPermissionState.status.isGranted && + !localNetworkPermissionState.status.shouldShowRationale + ) { + localNetworkPermissionState.launchPermissionRequest() + } else { + when (connectionRole) { + ConnectionRole.Host -> onHostClicked() + ConnectionRole.Connect -> onConnectClicked() + } + } }, ) { if (connecting) { @@ -238,6 +281,10 @@ private fun NetplaySetupContent( onUseUpnpChanged = onUseUpnpChanged, ) } + + LocalNetworkPermissionInfo( + connectionType = connectionType, + ) } @Composable @@ -374,8 +421,87 @@ private fun HostMenu( ) } } + } +} + +@Composable +private fun LocalNetworkPermissionInfo( + connectionType: ConnectionType, +) { + if ( + connectionType is ConnectionType.DirectConnection && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN + ) { + val context = LocalContext.current + + // Android permissions do not differentiate between isGranted being false because the user + // has never been asked and isGranted being false because the user denied twice. We only + // get shouldShowRationale which, if the user keeps denying goes false, true, always false. + // So if the value does not change after calling launchPermissionRequest we know we are in + // the final denied state and launch app settings for the user to manually grant from there. + var previousShouldShowRationale by rememberSaveable { mutableStateOf(false) } - MenuSpacer() + // Check if the permission dialog actually showed by detecting an onPause event, this allows + // us to detect if the dialog was dismissed with the back button which would prevent + // previousShouldShowRationale from toggling as described above. + var dialogShown by rememberSaveable { mutableStateOf(false) } + LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) { dialogShown = true } + + lateinit var permissionState: PermissionState + permissionState = + rememberPermissionState(Manifest.permission.ACCESS_LOCAL_NETWORK) { granted -> + if ( + !granted && + !dialogShown && + previousShouldShowRationale == permissionState.status.shouldShowRationale + ) { + context.startActivity( + Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", context.packageName, null) + ) + ) + } + previousShouldShowRationale = permissionState.status.shouldShowRationale + } + + if (!permissionState.status.isGranted) { + MenuSpacer() + + OutlinedBox( + label = { + Icon( + imageVector = Icons.Default.Warning, + tint = MaterialTheme.colorScheme.error, + contentDescription = null, + ) + } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + ) { + Text( + text = stringResource(R.string.netplay_local_network_permission_info), + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = { + previousShouldShowRationale = permissionState.status.shouldShowRationale + dialogShown = false + permissionState.launchPermissionRequest() + }, + modifier = Modifier.align(Alignment.End) + ) { + Text(text = stringResource(R.string.netplay_give_permission)) + } + } + } + } } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt index 210a34b23f..8a46a698ce 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt @@ -1359,7 +1359,7 @@ class SettingsFragmentPresenter( ) sl.add( IntSliderSetting( - context, mem2Size, R.string.main_mem2_size, 0, 64, 128, "MB", 1 + context, mem2Size, R.string.main_mem2_size, 0, 64, 256, "MB", 1 ) ) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt index a005995bdb..a0215d4cd8 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt @@ -165,6 +165,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { runWhenSurfaceIsValid = true } + fun resetEmulation() { + Log.debug("[EmulationFragment] Resetting emulation.") + NativeLibrary.ResetEmulation() + } + fun stopEmulation() { Log.debug("[EmulationFragment] Stopping emulation.") NativeLibrary.StopEmulation() diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt index 6bf9c7c147..ee4bb50e3f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt @@ -76,6 +76,7 @@ class MenuFragment : Fragment(), View.OnClickListener { button.setOnClickListener(this) } + binding.menuReset.setOnClickListener(this) binding.menuExit.setOnClickListener(this) val title = requireArguments().getString(KEY_TITLE, null) @@ -199,6 +200,7 @@ class MenuFragment : Fragment(), View.OnClickListener { EmulationActivity.MENU_ACTION_CHANGE_DISC ) buttonsActionsMap.append(R.id.menu_exit, EmulationActivity.MENU_ACTION_EXIT) + buttonsActionsMap.append(R.id.menu_reset, EmulationActivity.MENU_ACTION_RESET) buttonsActionsMap.append(R.id.menu_settings, EmulationActivity.MENU_ACTION_SETTINGS) buttonsActionsMap.append(R.id.menu_skylanders, EmulationActivity.MENU_ACTION_SKYLANDERS) buttonsActionsMap.append( diff --git a/Source/Android/app/src/main/res/layout/activity_settings.xml b/Source/Android/app/src/main/res/layout/activity_settings.xml index fbecfd3886..b4226e3d9f 100644 --- a/Source/Android/app/src/main/res/layout/activity_settings.xml +++ b/Source/Android/app/src/main/res/layout/activity_settings.xml @@ -140,10 +140,11 @@ </com.google.android.material.appbar.MaterialToolbar> - <View + <com.google.android.material.divider.MaterialDivider android:layout_width="match_parent" - android:layout_height="1dp" - android:background="@color/settings_search_outline" /> + android:layout_height="wrap_content" + app:dividerColor="@color/settings_search_outline" + app:dividerThickness="1dp" /> </LinearLayout> diff --git a/Source/Android/app/src/main/res/layout/fragment_ingame_menu.xml b/Source/Android/app/src/main/res/layout/fragment_ingame_menu.xml index 446706b821..39d992061e 100644 --- a/Source/Android/app/src/main/res/layout/fragment_ingame_menu.xml +++ b/Source/Android/app/src/main/res/layout/fragment_ingame_menu.xml @@ -111,6 +111,11 @@ android:text="@string/emulate_infinity_base" style="@style/InGameMenuOption" /> + <Button + android:id="@+id/menu_reset" + style="@style/InGameMenuOption" + android:text="@string/emulation_reset" /> + </LinearLayout> </ScrollView> diff --git a/Source/Android/app/src/main/res/values/dimens.xml b/Source/Android/app/src/main/res/values/dimens.xml index f10f34e81a..882f891bab 100644 --- a/Source/Android/app/src/main/res/values/dimens.xml +++ b/Source/Android/app/src/main/res/values/dimens.xml @@ -7,6 +7,8 @@ <dimen name="spacing_fab">72dp</dimen> <dimen name="menu_width">256dp</dimen> <dimen name="card_width">135dp</dimen> + <dimen name="tv_settings_card_width">120dp</dimen> + <dimen name="tv_settings_card_height">80dp</dimen> <dimen name="elevated_app_bar">3dp</dimen> </resources> diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 2204f4380f..f8099fd652 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -630,6 +630,7 @@ It can efficiently compress both junk data and encrypted Wii data. <string name="emulation_screenshot">Take Screenshot</string> <string name="emulation_savestate">Save State</string> <string name="emulation_loadstate">Load State</string> + <string name="emulation_reset">Reset Emulation</string> <string name="emulation_exit">Exit Emulation</string> <string name="emulation_state_slot">Slot %1$d\n\n%2$s</string> <string name="emulation_state_slot_empty">Slot %1$d\n\nEmpty</string> @@ -1062,4 +1063,6 @@ It can efficiently compress both junk data and encrypted Wii data. <string name="netplay_address_share">Share address</string> <string name="netplay_address_retry">Retry</string> <string name="netplay_transfer_progress">%1$s/%2$s MiB</string> + <string name="netplay_local_network_permission_info">Dolphin needs permission to connect to devices on a local network. If you only want to use netplay over the internet, the permission is not required.</string> + <string name="netplay_give_permission">Give permission</string> </resources> |
