diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..c94f2bccfe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,44 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Disclamer** +In order to keep this bug tracker manageable, you need to keep a few things in mind: +* Any issue not following this template will be closed as invalid +* Is the issue reproducible on RetroArch with the same core? If so, it's very likely a core issue and the upstream core repository is a better place to file it +* Is the issue already opened? Try out a search before filing one. + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..1b87167a35 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,25 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Disclamer** +In order to keep this bug tracker manageable, you need to keep a few things in mind: +*Any feature request not following this template will be closed as invalid +*Is the issue already opened? Try out a search before filing one. + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/README.md b/README.md index d86bd417f8..a09a749b34 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ It originated from a rib of [Retrograde](https://github.com/retrograde/retrograd - NEC PC Engine (PCE) ([beetle_pce_fast](https://docs.libretro.com/library/beetle_pce_fast/)) - Neo Geo Pocket (NGP) ([mednafen_ngp](https://docs.libretro.com/library/beetle_neopop/)) - Neo Geo Pocket Color (NGC) ([mednafen_ngp](https://docs.libretro.com/library/beetle_neopop/)) +- Nintendo 3DS (3DS) ([citra](https://docs.libretro.com/library/citra/)) ### Features: - Android TV support diff --git a/buildSrc/src/main/java/deps.kt b/buildSrc/src/main/java/deps.kt index 8dcd3b95b1..d05d7d088d 100644 --- a/buildSrc/src/main/java/deps.kt +++ b/buildSrc/src/main/java/deps.kt @@ -1,10 +1,10 @@ /* ktlint-disable no-multi-spaces max-line-length */ object deps { object android { - const val targetSdkVersion = 29 - const val compileSdkVersion = 29 + const val targetSdkVersion = 31 + const val compileSdkVersion = 31 const val minSdkVersion = 23 - const val buildToolsVersion = "29.0.3" + const val buildToolsVersion = "30.0.2" } object versions { @@ -15,7 +15,7 @@ object deps { const val kotlin = "1.4.30" const val okHttp = "4.9.1" const val retrofit = "2.9.0" - const val work = "2.5.0" + const val work = "2.7.1" const val navigation = "2.3.5" const val rxbindings = "3.1.0" const val lifecycle = "2.3.1" @@ -25,8 +25,8 @@ object deps { const val room = "2.3.0" const val epoxy = "4.6.3-vinay-compose" const val serialization = "1.2.2" - const val libretrodroid = "0.6.1" - const val radialgamepad = "0.5.0" + const val libretrodroid = "0.7.0" + const val radialgamepad = "0.6.1" } object libs { @@ -76,6 +76,7 @@ object deps { const val runtime = "androidx.work:work-runtime:${versions.work}" const val runtimeKtx = "androidx.work:work-runtime-ktx:${versions.work}" const val rxjava2 ="androidx.work:work-rxjava2:${versions.work}" + const val multiprocess ="androidx.work:work-multiprocess:${versions.work}" } } object autodispose { @@ -126,7 +127,7 @@ object deps { const val okio = "com.squareup.okio:okio:2.10.0" const val okHttp3 = "com.squareup.okhttp3:okhttp:${versions.okHttp}" const val okHttp3Logging = "com.squareup.okhttp3:logging-interceptor:${versions.okHttp}" - const val coil = "io.coil-kt:coil:1.3.2" + const val coil = "io.coil-kt:coil:1.4.0" const val retrofit = "com.squareup.retrofit2:retrofit:${versions.retrofit}" const val retrofitRxJava2 = "com.squareup.retrofit2:adapter-rxjava2:${versions.retrofit}" const val rxAndroid2 = "io.reactivex.rxjava2:rxandroid:2.1.1" @@ -136,10 +137,11 @@ object deps { const val rxPreferences = "com.f2prateek.rx.preferences2:rx-preferences:2.0.1" const val rxRelay2 = "com.jakewharton.rxrelay2:rxrelay:2.1.1" const val timber = "com.jakewharton.timber:timber:5.0.1" - const val material = "com.google.android.material:material:1.4.0" + const val material = "com.google.android.material:material:1.5.0" const val multitouchGestures = "com.dinuscxj:multitouchgesturedetector:1.0.0" const val guava = "com.google.guava:guava:30.1.1-android" const val harmony = "com.frybits.harmony:harmony:1.1.9" + const val startup = "androidx.startup:startup-runtime:1.1.1" const val radialgamepad = "com.github.Swordfish90:RadialGamePad:${versions.radialgamepad}" const val libretrodroid = "com.github.Swordfish90:LibretroDroid:${versions.libretrodroid}" } diff --git a/lemuroid-app-ext-free/build.gradle.kts b/lemuroid-app-ext-free/build.gradle.kts index 70c981f710..7e34231c20 100644 --- a/lemuroid-app-ext-free/build.gradle.kts +++ b/lemuroid-app-ext-free/build.gradle.kts @@ -4,6 +4,12 @@ plugins { id("kotlin-kapt") } +android { + kotlinOptions { + jvmTarget = "1.8" + } +} + dependencies { implementation(project(":retrograde-util")) implementation(project(":retrograde-app-shared")) diff --git a/lemuroid-app-ext-free/src/main/java/com/swordfish/lemuroid/ext/feature/core/CoreUpdaterImpl.kt b/lemuroid-app-ext-free/src/main/java/com/swordfish/lemuroid/ext/feature/core/CoreUpdaterImpl.kt index d47b999663..eb59bde738 100644 --- a/lemuroid-app-ext-free/src/main/java/com/swordfish/lemuroid/ext/feature/core/CoreUpdaterImpl.kt +++ b/lemuroid-app-ext-free/src/main/java/com/swordfish/lemuroid/ext/feature/core/CoreUpdaterImpl.kt @@ -43,7 +43,7 @@ class CoreUpdaterImpl( // This is the last tagged versions of cores. companion object { - private const val CORES_VERSION = "1.12" + private const val CORES_VERSION = "1.13" } private val baseUri = Uri.parse("https://github.com/Swordfish90/LemuroidCores/") diff --git a/lemuroid-app/build.gradle.kts b/lemuroid-app/build.gradle.kts index 95ac77ba23..b2282f768f 100644 --- a/lemuroid-app/build.gradle.kts +++ b/lemuroid-app/build.gradle.kts @@ -1,15 +1,14 @@ plugins { id("com.android.application") id("kotlin-android") - id("kotlin-android-extensions") id("kotlin-kapt") id("androidx.navigation.safeargs.kotlin") } android { defaultConfig { - versionCode = 155 - versionName = "1.12.1" + versionCode = 161 + versionName = "1.13.0" // Always remember to update Cores Tag! applicationId = "com.swordfish.lemuroid" } @@ -34,7 +33,8 @@ android { ":lemuroid_core_ppsspp", ":lemuroid_core_prosystem", ":lemuroid_core_snes9x", - ":lemuroid_core_stella" + ":lemuroid_core_stella", + ":lemuroid_core_citra" ) } @@ -88,15 +88,11 @@ android { signingConfig = signingConfigs["release"] proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") resValue("string", "lemuroid_name", "Lemuroid") - resValue("color", "main_color", "#00c64e") - resValue("color", "main_color_light", "#9de3aa") } getByName("debug") { applicationIdSuffix = ".debug" versionNameSuffix = "-DEBUG" resValue("string", "lemuroid_name", "LemuroiDebug") - resValue("color", "main_color", "#f44336") - resValue("color", "main_color_light", "#ef9a9a") } } @@ -174,6 +170,7 @@ dependencies { implementation(deps.libs.androidx.documentfile) implementation(deps.libs.androidx.leanback.tvProvider) implementation(deps.libs.harmony) + implementation(deps.libs.startup) implementation(deps.libs.libretrodroid) diff --git a/lemuroid-app/src/debug/res/values/leanback-colors.xml b/lemuroid-app/src/debug/res/values/leanback-colors.xml new file mode 100644 index 0000000000..8a88477a2c --- /dev/null +++ b/lemuroid-app/src/debug/res/values/leanback-colors.xml @@ -0,0 +1,5 @@ + + @color/surface_elevation_0dp + @color/surface_elevation_3dp + @color/surface_elevation_1dp + diff --git a/lemuroid-app/src/debug/res/values/material-colors.xml b/lemuroid-app/src/debug/res/values/material-colors.xml new file mode 100644 index 0000000000..977056abc0 --- /dev/null +++ b/lemuroid-app/src/debug/res/values/material-colors.xml @@ -0,0 +1,62 @@ + + #f44336 + #ef9a9a + + + #BC1714 + #FFFFFF + #FFDAD3 + #410001 + #775652 + #FFFFFF + #FFDAD4 + #2C1512 + #715C2E + #FFFFFF + #FCDFA6 + #261A00 + #BA1B1B + #FFDAD4 + #FFFFFF + #410001 + #FCFCFC + #211A19 + #FCFCFC + #211A19 + #F4DDDA + #534341 + #857370 + #FBEEEC + #362F2E + #FFB4A8 + #000000 + #FFB4A8 + #FFB4A8 + #680001 + #940002 + #FFDAD3 + #E7BCB6 + #442A26 + #5D3F3B + #FFDAD4 + #DFC38C + #3F2E04 + #574419 + #FCDFA6 + #FFB4A9 + #930006 + #680003 + #FFDAD4 + #211A19 + #EDE0DE + #211A19 + #EDE0DE + #534341 + #D8C2BF + #A08C89 + #211A19 + #EDE0DE + #BC1714 + #000000 + #BC1714 + diff --git a/lemuroid-app/src/main/AndroidManifest.xml b/lemuroid-app/src/main/AndroidManifest.xml index 468ebe640b..3d3c10a155 100644 --- a/lemuroid-app/src/main/AndroidManifest.xml +++ b/lemuroid-app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -8,6 +9,7 @@ + + android:theme="@style/LemuroidMaterialTheme"> + android:name="com.swordfish.lemuroid.app.mobile.feature.main.MainActivity" + android:exported="true"> @@ -33,7 +36,8 @@ + android:exported="true" + android:theme="@style/LemuroidMaterialTheme.Game"> @@ -45,21 +49,22 @@ android:process=":game" android:launchMode="singleInstance" android:configChanges="orientation|keyboardHidden|screenSize" - android:theme="@style/GameTheme"/> + android:theme="@style/LemuroidMaterialTheme.Game"/> + android:theme="@style/LemuroidMaterialTheme.Menu"/> + android:theme="@style/LemuroidMaterialTheme.Invisible"/> + android:exported="true" + android:theme="@style/LemuroidLeanbackTheme"> @@ -72,7 +77,7 @@ + android:theme="@style/LemuroidLeanbackPreferencesTheme" /> @@ -80,22 +85,27 @@ + android:theme="@style/LemuroidMaterialTheme.Game" /> + + - + android:name="androidx.startup.InitializationProvider" + android:authorities="${applicationId}.androidx-startup" + android:exported="false" + tools:node="merge"> + + + diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/LemuroidApplication.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/LemuroidApplication.kt index d3ceacf345..3042989905 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/LemuroidApplication.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/LemuroidApplication.kt @@ -1,29 +1,17 @@ package com.swordfish.lemuroid.app import android.annotation.SuppressLint -import android.app.ActivityManager import android.content.Context -import android.os.Build -import android.os.Process -import android.os.StrictMode -import androidx.work.Configuration import androidx.work.ListenableWorker -import androidx.work.WorkManager -import com.swordfish.lemuroid.BuildConfig -import com.swordfish.lemuroid.app.shared.library.LibraryIndexScheduler -import com.swordfish.lemuroid.app.shared.savesync.SaveSyncWork +import com.google.android.material.color.DynamicColors import com.swordfish.lemuroid.ext.feature.context.ContextHandler import com.swordfish.lemuroid.lib.injection.HasWorkerInjector import dagger.android.AndroidInjector import dagger.android.DispatchingAndroidInjector import dagger.android.support.DaggerApplication -import timber.log.Timber import javax.inject.Inject class LemuroidApplication : DaggerApplication(), HasWorkerInjector { - companion object { - fun get(context: Context) = context.applicationContext as LemuroidApplication - } /*@Inject lateinit var rxTimberTree: RxTimberTree @@ -38,12 +26,7 @@ class LemuroidApplication : DaggerApplication(), HasWorkerInjector { override fun onCreate() { super.onCreate() - initializeWorkManager() - - if (BuildConfig.DEBUG) { - Timber.plant(Timber.DebugTree()) - enableStrictMode() - } + DynamicColors.applyToActivitiesIfAvailable(this) // var isPlanted = false /* rxPrefs.getBoolean(getString(R.string.pref_key_flags_logging)).asObservable() @@ -62,44 +45,6 @@ class LemuroidApplication : DaggerApplication(), HasWorkerInjector { }*/ } - private fun enableStrictMode() { - StrictMode.setThreadPolicy( - StrictMode.ThreadPolicy.Builder() - .detectAll() - .penaltyLog() - .build() - ) - } - - private fun initializeWorkManager() { - val config = Configuration.Builder() - .setMinimumLoggingLevel(android.util.Log.INFO) - .build() - - WorkManager.initialize(this, config) - - if (isMainProcess()) { - SaveSyncWork.enqueueAutoWork(applicationContext, 0) - LibraryIndexScheduler.scheduleCoreUpdate(applicationContext) - } - } - - private fun isMainProcess(): Boolean { - return retrieveProcessName() == packageName - } - - private fun retrieveProcessName(): String? { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - return getProcessName() - } - - val currentPID = Process.myPid() - val manager = getSystemService(ACTIVITY_SERVICE) as ActivityManager - return manager.runningAppProcesses - .firstOrNull { it.pid == currentPID } - ?.processName - } - override fun attachBaseContext(base: Context) { super.attachBaseContext(base) ContextHandler.attachBaseContext(base) diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/LemuroidApplicationComponent.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/LemuroidApplicationComponent.kt index cd6096275e..5d0fc0375d 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/LemuroidApplicationComponent.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/LemuroidApplicationComponent.kt @@ -22,6 +22,7 @@ package com.swordfish.lemuroid.app import com.swordfish.lemuroid.app.shared.library.CoreUpdateWork import com.swordfish.lemuroid.app.shared.library.LibraryIndexWork import com.swordfish.lemuroid.app.shared.savesync.SaveSyncWork +import com.swordfish.lemuroid.app.shared.storage.cache.CacheCleanerWork import com.swordfish.lemuroid.app.tv.LemuroidTVApplicationModule import com.swordfish.lemuroid.app.tv.channel.ChannelUpdateWork import com.swordfish.lemuroid.lib.injection.AndroidWorkerInjectionModule @@ -39,6 +40,7 @@ import dagger.android.support.AndroidSupportInjectionModule SaveSyncWork.Module::class, ChannelUpdateWork.Module::class, CoreUpdateWork.Module::class, + CacheCleanerWork.Module::class, LemuroidTVApplicationModule::class ] ) diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/LemuroidApplicationModule.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/LemuroidApplicationModule.kt index 13d67ab3d0..7b7b502487 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/LemuroidApplicationModule.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/LemuroidApplicationModule.kt @@ -27,13 +27,15 @@ import com.swordfish.lemuroid.app.mobile.feature.gamemenu.GameMenuActivity import com.swordfish.lemuroid.app.mobile.feature.main.MainActivity import com.swordfish.lemuroid.app.mobile.feature.settings.RxSettingsManager import com.swordfish.lemuroid.app.mobile.feature.shortcuts.ShortcutsGenerator +import com.swordfish.lemuroid.app.shared.covers.CoverLoader +import com.swordfish.lemuroid.app.shared.rumble.RumbleManager import com.swordfish.lemuroid.app.shared.game.ExternalGameLauncherActivity import com.swordfish.lemuroid.app.shared.game.GameLauncher -import com.swordfish.lemuroid.app.shared.main.PostGameHandler +import com.swordfish.lemuroid.app.shared.main.GameLaunchTaskHandler import com.swordfish.lemuroid.app.shared.settings.BiosPreferences import com.swordfish.lemuroid.app.shared.settings.ControllerConfigsManager import com.swordfish.lemuroid.app.shared.settings.CoresSelectionPreferences -import com.swordfish.lemuroid.app.shared.settings.GamePadManager +import com.swordfish.lemuroid.app.shared.input.InputDeviceManager import com.swordfish.lemuroid.app.shared.settings.GamePadPreferencesHelper import com.swordfish.lemuroid.app.shared.settings.StorageFrameworkPickerLauncher import com.swordfish.lemuroid.app.tv.channel.ChannelHandler @@ -281,7 +283,7 @@ abstract class LemuroidApplicationModule { @PerApp @JvmStatic fun gamepadsManager(context: Context, sharedPreferences: Lazy) = - GamePadManager(context, sharedPreferences) + InputDeviceManager(context, sharedPreferences) @Provides @PerApp @@ -306,8 +308,8 @@ abstract class LemuroidApplicationModule { @Provides @PerApp @JvmStatic - fun gamepadSettingsPreferences(gamePadManager: GamePadManager) = - GamePadPreferencesHelper(gamePadManager) + fun inputDeviceManager(inputDeviceManager: InputDeviceManager) = + GamePadPreferencesHelper(inputDeviceManager) @Provides @PerApp @@ -327,7 +329,7 @@ abstract class LemuroidApplicationModule { @PerApp @JvmStatic fun postGameHandler(retrogradeDatabase: RetrogradeDatabase) = - PostGameHandler(ReviewManager(), retrogradeDatabase) + GameLaunchTaskHandler(ReviewManager(), retrogradeDatabase) @Provides @PerApp @@ -366,7 +368,27 @@ abstract class LemuroidApplicationModule { @Provides @PerApp @JvmStatic - fun gameLauncher(coresSelection: CoresSelection) = - GameLauncher(coresSelection) + fun gameLauncher( + coresSelection: CoresSelection, + gameLaunchTaskHandler: GameLaunchTaskHandler + ) = + GameLauncher(coresSelection, gameLaunchTaskHandler) + + @Provides + @PerApp + @JvmStatic + fun rumbleManager( + context: Context, + rxSettingsManager: RxSettingsManager, + inputDeviceManager: InputDeviceManager + ) = + RumbleManager(context, rxSettingsManager, inputDeviceManager) + + @Provides + @PerApp + @JvmStatic + fun coverLoader( + context: Context + ) = CoverLoader(context) } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/favorites/FavoritesFragment.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/favorites/FavoritesFragment.kt index 7a3c5271cf..ac0311586b 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/favorites/FavoritesFragment.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/favorites/FavoritesFragment.kt @@ -10,14 +10,16 @@ import com.swordfish.lemuroid.app.mobile.shared.GamesAdapter import com.swordfish.lemuroid.app.mobile.shared.GridSpaceDecoration import com.swordfish.lemuroid.app.mobile.shared.RecyclerViewFragment import com.swordfish.lemuroid.app.shared.GameInteractor +import com.swordfish.lemuroid.app.shared.covers.CoverLoader import com.swordfish.lemuroid.lib.library.db.RetrogradeDatabase -import com.swordfish.lemuroid.lib.ui.setVisibleOrGone +import com.swordfish.lemuroid.common.view.setVisibleOrGone import javax.inject.Inject class FavoritesFragment : RecyclerViewFragment() { @Inject lateinit var retrogradeDb: RetrogradeDatabase @Inject lateinit var gameInteractor: GameInteractor + @Inject lateinit var coverLoader: CoverLoader private lateinit var favoritesViewModel: FavoritesViewModel @@ -27,7 +29,7 @@ class FavoritesFragment : RecyclerViewFragment() { favoritesViewModel = ViewModelProvider(this, FavoritesViewModel.Factory(retrogradeDb)) .get(FavoritesViewModel::class.java) - val gamesAdapter = GamesAdapter(R.layout.layout_game_grid, gameInteractor) + val gamesAdapter = GamesAdapter(R.layout.layout_game_grid, gameInteractor, coverLoader) favoritesViewModel.favorites.cachedIn(lifecycle).observe(viewLifecycleOwner) { gamesAdapter.submitData(lifecycle, it) } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/game/GameActivity.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/game/GameActivity.kt index a622add241..e41281c3ce 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/game/GameActivity.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/game/GameActivity.kt @@ -19,7 +19,6 @@ package com.swordfish.lemuroid.app.mobile.feature.game -import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.res.Configuration @@ -45,34 +44,32 @@ import com.swordfish.lemuroid.common.math.linearInterpolation import com.swordfish.lemuroid.common.rx.BehaviorRelayNullableProperty import com.swordfish.lemuroid.common.rx.BehaviorRelayProperty import com.swordfish.lemuroid.common.rx.RXUtils +import com.swordfish.lemuroid.common.view.setVisibleOrGone import com.swordfish.lemuroid.lib.controller.ControllerConfig -import com.swordfish.lemuroid.lib.ui.setVisibleOrGone +import com.swordfish.lemuroid.lib.controller.TouchControllerCustomizer +import com.swordfish.lemuroid.lib.controller.TouchControllerSettingsManager import com.swordfish.lemuroid.lib.util.subscribeBy import com.swordfish.libretrodroid.GLRetroView import com.swordfish.radialgamepad.library.RadialGamePad -import com.swordfish.radialgamepad.library.config.RadialGamePadConfig -import com.swordfish.radialgamepad.library.config.RadialGamePadTheme import com.swordfish.radialgamepad.library.event.Event import com.swordfish.radialgamepad.library.event.GestureType -import com.swordfish.touchinput.radial.RadialPadConfigs -import com.swordfish.lemuroid.lib.controller.TouchControllerCustomizer -import com.swordfish.lemuroid.lib.controller.TouchControllerSettingsManager import com.swordfish.radialgamepad.library.haptics.HapticConfig +import com.swordfish.touchinput.radial.LemuroidTouchConfigs import com.swordfish.touchinput.radial.sensors.TiltSensor import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDispose +import dagger.Lazy import io.reactivex.Completable import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.Observables import io.reactivex.rxkotlin.subscribeBy +import io.reactivex.schedulers.Schedulers import timber.log.Timber +import java.util.concurrent.TimeUnit import javax.inject.Inject import kotlin.math.roundToInt -import dagger.Lazy -import io.reactivex.rxkotlin.Observables -import io.reactivex.schedulers.Schedulers -import java.util.concurrent.TimeUnit class GameActivity : BaseGameActivity() { @Inject lateinit var sharedPreferences: Lazy @@ -100,6 +97,7 @@ class GameActivity : BaseGameActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + orientation = getCurrentOrientation() tiltSensor = TiltSensor(applicationContext) @@ -154,8 +152,8 @@ class GameActivity : BaseGameActivity() { } private fun isVirtualGamePadVisible(): Observable { - return gamePadManager - .getEnabledGamePadsObservable() + return inputDeviceManager + .getEnabledInputsObservable() .map { it.isEmpty() } } @@ -182,31 +180,36 @@ class GameActivity : BaseGameActivity() { else -> HapticConfig.OFF } - val leftPad = RadialGamePad( - wrapGamePadConfig( - applicationContext, - touchControllerConfig.leftConfig, - hapticConfig - ), - DEFAULT_MARGINS_DP, - this + val leftConfig = LemuroidTouchConfigs.getRadialGamePadConfig( + touchControllerConfig.leftConfig, + hapticConfig, + leftGamePadContainer ) + val leftPad = RadialGamePad(leftConfig, DEFAULT_MARGINS_DP, this) leftGamePadContainer.addView(leftPad) - val rightPad = RadialGamePad( - wrapGamePadConfig( - applicationContext, - touchControllerConfig.rightConfig, - hapticConfig - ), - DEFAULT_MARGINS_DP, - this + val rightConfig = LemuroidTouchConfigs.getRadialGamePadConfig( + touchControllerConfig.rightConfig, + hapticConfig, + leftGamePadContainer ) + val rightPad = RadialGamePad(rightConfig, DEFAULT_MARGINS_DP, this) rightGamePadContainer.addView(rightPad) val virtualPadEvents = Observable.merge(leftPad.events(), rightPad.events()) .share() + setupDefaultActions(virtualPadEvents) + setupTiltActions(virtualPadEvents) + setupVirtualMenuActions(virtualPadEvents) + + this.leftPad = leftPad + this.rightPad = rightPad + + this.touchControllerConfig = controllerConfig + } + + private fun setupDefaultActions(virtualPadEvents: Observable) { virtualControllerDisposables.add( virtualPadEvents .subscribeBy { @@ -217,13 +220,12 @@ class GameActivity : BaseGameActivity() { is Event.Direction -> { handleGamePadDirection(it) } - is Event.Gesture -> { - handleGamePadGesture(it) - } } } ) + } + private fun setupTiltActions(virtualPadEvents: Observable) { virtualControllerDisposables.add( virtualPadEvents .ofType(Event.Gesture::class.java) @@ -249,29 +251,54 @@ class GameActivity : BaseGameActivity() { } } ) + } - this.leftPad = leftPad - this.rightPad = rightPad + private fun setupVirtualMenuActions(virtualPadEvents: Observable) { + VirtualLongPressHandler.initializeTheme(this) - this.touchControllerConfig = controllerConfig + val allMenuButtonEvents = virtualPadEvents + .ofType(Event.Button::class.java) + .filter { it.id == KeyEvent.KEYCODE_BUTTON_MODE } + .share() + + val cancelMenuButtonEvents = allMenuButtonEvents + .filter { it.action == KeyEvent.ACTION_UP } + .map { Unit } + + virtualControllerDisposables.add( + allMenuButtonEvents + .filter { it.action == KeyEvent.ACTION_DOWN } + .concatMapMaybe { + VirtualLongPressHandler.displayLoading( + this, + R.drawable.ic_menu, + cancelMenuButtonEvents + ) + .doOnSuccess { + displayOptionsDialog() + simulateVirtualGamepadHaptic() + } + } + .subscribeBy(Timber::e) { } + ) } private fun handleTripleTaps(events: MutableList) { val eventsTracker = when (events.map { it.id }.toSet()) { - setOf(RadialPadConfigs.MOTION_SOURCE_LEFT_STICK) -> StickTiltTracker( - RadialPadConfigs.MOTION_SOURCE_LEFT_STICK + setOf(LemuroidTouchConfigs.MOTION_SOURCE_LEFT_STICK) -> StickTiltTracker( + LemuroidTouchConfigs.MOTION_SOURCE_LEFT_STICK ) - setOf(RadialPadConfigs.MOTION_SOURCE_RIGHT_STICK) -> StickTiltTracker( - RadialPadConfigs.MOTION_SOURCE_RIGHT_STICK + setOf(LemuroidTouchConfigs.MOTION_SOURCE_RIGHT_STICK) -> StickTiltTracker( + LemuroidTouchConfigs.MOTION_SOURCE_RIGHT_STICK ) - setOf(RadialPadConfigs.MOTION_SOURCE_DPAD) -> CrossTiltTracker( - RadialPadConfigs.MOTION_SOURCE_DPAD + setOf(LemuroidTouchConfigs.MOTION_SOURCE_DPAD) -> CrossTiltTracker( + LemuroidTouchConfigs.MOTION_SOURCE_DPAD ) - setOf(RadialPadConfigs.MOTION_SOURCE_DPAD_AND_LEFT_STICK) -> CrossTiltTracker( - RadialPadConfigs.MOTION_SOURCE_DPAD_AND_LEFT_STICK + setOf(LemuroidTouchConfigs.MOTION_SOURCE_DPAD_AND_LEFT_STICK) -> CrossTiltTracker( + LemuroidTouchConfigs.MOTION_SOURCE_DPAD_AND_LEFT_STICK ) - setOf(RadialPadConfigs.MOTION_SOURCE_RIGHT_DPAD) -> CrossTiltTracker( - RadialPadConfigs.MOTION_SOURCE_RIGHT_DPAD + setOf(LemuroidTouchConfigs.MOTION_SOURCE_RIGHT_DPAD) -> CrossTiltTracker( + LemuroidTouchConfigs.MOTION_SOURCE_RIGHT_DPAD ) setOf( KeyEvent.KEYCODE_BUTTON_L1, @@ -312,53 +339,30 @@ class GameActivity : BaseGameActivity() { stopGameService() } - private fun getGamePadTheme(context: Context): RadialGamePadTheme { - val accentColor = GraphicsUtils.colorToRgb(context.getColor(R.color.colorPrimary)) - val alpha = (255 * PRESSED_COLOR_ALPHA).roundToInt() - val pressedColor = GraphicsUtils.rgbaToColor(accentColor + listOf(alpha)) - val simulatedColor = GraphicsUtils.rgbaToColor(accentColor + (255 * 0.25f).roundToInt()) - return RadialGamePadTheme( - normalColor = context.getColor(R.color.touch_control_normal), - pressedColor = pressedColor, - simulatedColor = simulatedColor, - primaryDialBackground = context.getColor(R.color.touch_control_background), - textColor = context.getColor(R.color.touch_control_text) - ) - } - - private fun wrapGamePadConfig( - context: Context, - config: RadialGamePadConfig, - hapticConfig: HapticConfig - ): RadialGamePadConfig { - val padTheme = getGamePadTheme(context) - return config.copy(theme = padTheme, haptic = hapticConfig) - } - private fun handleGamePadButton(it: Event.Button) { retroGameView?.sendKeyEvent(it.action, it.id) } private fun handleGamePadDirection(it: Event.Direction) { when (it.id) { - RadialPadConfigs.MOTION_SOURCE_DPAD -> { + LemuroidTouchConfigs.MOTION_SOURCE_DPAD -> { retroGameView?.sendMotionEvent(GLRetroView.MOTION_SOURCE_DPAD, it.xAxis, it.yAxis) } - RadialPadConfigs.MOTION_SOURCE_LEFT_STICK -> { + LemuroidTouchConfigs.MOTION_SOURCE_LEFT_STICK -> { retroGameView?.sendMotionEvent( GLRetroView.MOTION_SOURCE_ANALOG_LEFT, it.xAxis, it.yAxis ) } - RadialPadConfigs.MOTION_SOURCE_RIGHT_STICK -> { + LemuroidTouchConfigs.MOTION_SOURCE_RIGHT_STICK -> { retroGameView?.sendMotionEvent( GLRetroView.MOTION_SOURCE_ANALOG_RIGHT, it.xAxis, it.yAxis ) } - RadialPadConfigs.MOTION_SOURCE_DPAD_AND_LEFT_STICK -> { + LemuroidTouchConfigs.MOTION_SOURCE_DPAD_AND_LEFT_STICK -> { retroGameView?.sendMotionEvent( GLRetroView.MOTION_SOURCE_ANALOG_LEFT, it.xAxis, @@ -366,7 +370,7 @@ class GameActivity : BaseGameActivity() { ) retroGameView?.sendMotionEvent(GLRetroView.MOTION_SOURCE_DPAD, it.xAxis, it.yAxis) } - RadialPadConfigs.MOTION_SOURCE_RIGHT_DPAD -> { + LemuroidTouchConfigs.MOTION_SOURCE_RIGHT_DPAD -> { retroGameView?.sendMotionEvent( GLRetroView.MOTION_SOURCE_ANALOG_RIGHT, it.xAxis, @@ -376,13 +380,6 @@ class GameActivity : BaseGameActivity() { } } - private fun handleGamePadGesture(it: Event.Gesture) { - if (it.id == KeyEvent.KEYCODE_BUTTON_MODE) { - displayOptionsDialog() - simulateVirtualGamepadHaptic() - } - } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) @@ -722,6 +719,9 @@ class GameActivity : BaseGameActivity() { leftPad.gravityX = -1f rightPad.gravityX = 1f + leftPad.secondaryDialSpacing = 0.1f + rightPad.secondaryDialSpacing = 0.1f + val constrainHeight = if (orientation == Configuration.ORIENTATION_PORTRAIT) { ConstraintSet.WRAP_CONTENT } else { @@ -767,9 +767,6 @@ class GameActivity : BaseGameActivity() { companion object { const val DEFAULT_MARGINS_DP = 8f - - const val PRESSED_COLOR_ALPHA = 0.5f - const val DEFAULT_PRIMARY_DIAL_SIZE = 160f } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/game/VirtualLongPressHandler.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/game/VirtualLongPressHandler.kt new file mode 100644 index 0000000000..30e1ed6d84 --- /dev/null +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/game/VirtualLongPressHandler.kt @@ -0,0 +1,74 @@ +package com.swordfish.lemuroid.app.mobile.feature.game + +import android.graphics.drawable.GradientDrawable +import android.view.View +import android.view.ViewConfiguration +import android.widget.ImageView +import com.google.android.material.progressindicator.CircularProgressIndicator +import com.swordfish.lemuroid.R +import com.swordfish.lemuroid.common.view.animateProgress +import com.swordfish.lemuroid.common.view.animateVisibleOrGone +import com.swordfish.lemuroid.common.view.setVisibleOrGone +import com.swordfish.touchinput.radial.LemuroidTouchOverlayThemes +import io.reactivex.Maybe +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import java.util.concurrent.TimeUnit + +object VirtualLongPressHandler { + + private val APPEAR_ANIMATION = (ViewConfiguration.getLongPressTimeout() * 0.1f).toLong() + private val DISAPPEAR_ANIMATION = (ViewConfiguration.getLongPressTimeout() * 2f).toLong() + private val LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout().toLong() + + fun initializeTheme(gameActivity: GameActivity) { + val palette = LemuroidTouchOverlayThemes.getGamePadAlternate(longPressView(gameActivity)) + longPressIconView(gameActivity).setColorFilter(palette.textColor) + longPressProgressBar(gameActivity).setIndicatorColor(palette.textColor) + longPressView(gameActivity).background = GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(palette.normalColor) + } + } + + fun displayLoading( + activity: GameActivity, + iconId: Int, + cancellation: Observable + ): Maybe { + return Observable.timer(LONG_PRESS_TIMEOUT, TimeUnit.MILLISECONDS) + .takeUntil(cancellation) + .firstElement() + .observeOn(AndroidSchedulers.mainThread()) + .doOnSubscribe { + longPressView(activity).alpha = 0f + longPressIconView(activity).setImageResource(iconId) + } + .doAfterSuccess { + longPressView(activity).setVisibleOrGone(false) + } + .doOnSubscribe { displayLongPressView(activity) } + .doAfterTerminate { hideLongPressView(activity) } + .onErrorComplete() + .map { Unit } + } + + private fun longPressIconView(activity: GameActivity) = + activity.findViewById(R.id.settings_loading_icon) + + private fun longPressProgressBar(activity: GameActivity) = + activity.findViewById(R.id.settings_loading_progress) + + private fun longPressView(activity: GameActivity) = + activity.findViewById(R.id.settings_loading) + + private fun displayLongPressView(activity: GameActivity) { + longPressView(activity).animateVisibleOrGone(true, APPEAR_ANIMATION) + longPressProgressBar(activity).animateProgress(100, LONG_PRESS_TIMEOUT) + } + + private fun hideLongPressView(activity: GameActivity) { + longPressView(activity).animateVisibleOrGone(false, DISAPPEAR_ANIMATION) + longPressProgressBar(activity).animateProgress(0, LONG_PRESS_TIMEOUT) + } +} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/gamemenu/GameMenuCoreOptionsFragment.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/gamemenu/GameMenuCoreOptionsFragment.kt index 2f09ca289a..9b3f3a302e 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/gamemenu/GameMenuCoreOptionsFragment.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/gamemenu/GameMenuCoreOptionsFragment.kt @@ -7,7 +7,7 @@ import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.shared.GameMenuContract import com.swordfish.lemuroid.app.shared.coreoptions.CoreOptionsPreferenceHelper import com.swordfish.lemuroid.app.shared.coreoptions.LemuroidCoreOption -import com.swordfish.lemuroid.app.shared.settings.GamePadManager +import com.swordfish.lemuroid.app.shared.input.InputDeviceManager import com.swordfish.lemuroid.lib.library.SystemCoreConfig import com.swordfish.lemuroid.lib.library.db.entity.Game import com.swordfish.lemuroid.lib.preferences.SharedPreferencesHelper @@ -21,7 +21,7 @@ import javax.inject.Inject class GameMenuCoreOptionsFragment : PreferenceFragmentCompat() { - @Inject lateinit var gamePadManager: GamePadManager + @Inject lateinit var inputDeviceManager: InputDeviceManager override fun onAttach(context: Context) { AndroidSupportInjection.inject(this) @@ -37,7 +37,7 @@ class GameMenuCoreOptionsFragment : PreferenceFragmentCompat() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - gamePadManager + inputDeviceManager .getGamePadsObservable() .observeOn(AndroidSchedulers.mainThread()) .autoDispose(scope()) diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/games/GamesFragment.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/games/GamesFragment.kt index 9b4b701abf..0f94680b9f 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/games/GamesFragment.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/games/GamesFragment.kt @@ -9,6 +9,7 @@ import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.mobile.shared.GamesAdapter import com.swordfish.lemuroid.app.mobile.shared.RecyclerViewFragment import com.swordfish.lemuroid.app.shared.GameInteractor +import com.swordfish.lemuroid.app.shared.covers.CoverLoader import com.swordfish.lemuroid.lib.library.db.RetrogradeDatabase import javax.inject.Inject @@ -16,6 +17,7 @@ class GamesFragment : RecyclerViewFragment() { @Inject lateinit var retrogradeDb: RetrogradeDatabase @Inject lateinit var gameInteractor: GameInteractor + @Inject lateinit var coverLoader: CoverLoader private val args: GamesFragmentArgs by navArgs() @@ -26,7 +28,7 @@ class GamesFragment : RecyclerViewFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - gamesAdapter = GamesAdapter(R.layout.layout_game_list, gameInteractor) + gamesAdapter = GamesAdapter(R.layout.layout_game_list, gameInteractor, coverLoader) gamesViewModel = ViewModelProvider(this, GamesViewModel.Factory(retrogradeDb)) .get(GamesViewModel::class.java) diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/EpoxyGameView.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/EpoxyGameView.kt index 65baf13fc6..86b5e39b41 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/EpoxyGameView.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/EpoxyGameView.kt @@ -23,11 +23,14 @@ abstract class EpoxyGameView : EpoxyModelWithHolder() { @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var gameInteractor: GameInteractor + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + lateinit var coverLoader: CoverLoader + override fun bind(holder: Holder) { holder.titleView?.text = game.title holder.subtitleView?.let { it.text = GameUtils.getGameSubtitle(it.context, game) } - CoverLoader.loadCover(game, holder.coverView) + coverLoader.loadCover(game, holder.coverView) holder.itemView?.setOnClickListener { gameInteractor.onGamePlay(game) } holder.itemView?.setOnCreateContextMenuListener( @@ -38,7 +41,7 @@ abstract class EpoxyGameView : EpoxyModelWithHolder() { override fun unbind(holder: Holder) { holder.itemView?.setOnClickListener(null) holder.coverView?.apply { - CoverLoader.cancelRequest(this) + coverLoader.cancelRequest(this) } holder.itemView?.setOnCreateContextMenuListener(null) } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/EpoxyHomeController.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/EpoxyHomeController.kt index 663355b21e..5062f7cfc0 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/EpoxyHomeController.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/EpoxyHomeController.kt @@ -6,12 +6,14 @@ import com.swordfish.lemuroid.BuildConfig import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.mobile.shared.withModelsFrom import com.swordfish.lemuroid.app.shared.GameInteractor +import com.swordfish.lemuroid.app.shared.covers.CoverLoader import com.swordfish.lemuroid.app.shared.settings.SettingsInteractor import com.swordfish.lemuroid.lib.library.db.entity.Game class EpoxyHomeController( private val gameInteractor: GameInteractor, - private val settingsInteractor: SettingsInteractor + private val settingsInteractor: SettingsInteractor, + private val coverLoader: CoverLoader ) : AsyncEpoxyController() { private var recentGames = listOf() @@ -71,6 +73,7 @@ class EpoxyHomeController( .id(item.id) .game(item) .gameInteractor(this@EpoxyHomeController.gameInteractor) + .coverLoader(this@EpoxyHomeController.coverLoader) } } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/HomeFragment.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/HomeFragment.kt index fbbcd41abb..7205b06846 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/HomeFragment.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/HomeFragment.kt @@ -12,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.Carousel import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.shared.GameInteractor +import com.swordfish.lemuroid.app.shared.covers.CoverLoader import com.swordfish.lemuroid.app.shared.settings.SettingsInteractor import com.swordfish.lemuroid.lib.library.db.RetrogradeDatabase import dagger.android.support.AndroidSupportInjection @@ -21,6 +22,7 @@ class HomeFragment : Fragment() { @Inject lateinit var retrogradeDb: RetrogradeDatabase @Inject lateinit var gameInteractor: GameInteractor + @Inject lateinit var coverLoader: CoverLoader @Inject lateinit var settingsInteractor: SettingsInteractor override fun onAttach(context: Context) { @@ -48,7 +50,7 @@ class HomeFragment : Fragment() { // Disable snapping in carousel view Carousel.setDefaultGlobalSnapHelperFactory(null) - val pagingController = EpoxyHomeController(gameInteractor, settingsInteractor) + val pagingController = EpoxyHomeController(gameInteractor, settingsInteractor, coverLoader) val recyclerView = view.findViewById(R.id.home_recyclerview) val layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/HomeViewModel.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/HomeViewModel.kt index ad4c81a495..e1b0617703 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/HomeViewModel.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/HomeViewModel.kt @@ -3,7 +3,7 @@ package com.swordfish.lemuroid.app.mobile.feature.home import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.swordfish.lemuroid.app.shared.library.LibraryIndexMonitor +import com.swordfish.lemuroid.app.shared.library.PendingOperationsMonitor import com.swordfish.lemuroid.lib.library.db.RetrogradeDatabase class HomeViewModel(appContext: Context, retrogradeDb: RetrogradeDatabase) : ViewModel() { @@ -24,5 +24,5 @@ class HomeViewModel(appContext: Context, retrogradeDb: RetrogradeDatabase) : Vie val recentGames = retrogradeDb.gameDao().selectFirstUnfavoriteRecents(CAROUSEL_MAX_ITEMS) - val indexingInProgress = LibraryIndexMonitor(appContext).getLiveData() + val indexingInProgress = PendingOperationsMonitor(appContext).anyLibraryOperationInProgress() } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainActivity.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainActivity.kt index d3b984fa74..45b33156af 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainActivity.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainActivity.kt @@ -14,6 +14,7 @@ import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.android.material.elevation.SurfaceColors import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.mobile.feature.favorites.FavoritesFragment import com.swordfish.lemuroid.app.mobile.feature.games.GamesFragment @@ -31,7 +32,8 @@ import com.swordfish.lemuroid.app.shared.GameInteractor import com.swordfish.lemuroid.app.shared.game.BaseGameActivity import com.swordfish.lemuroid.app.shared.game.GameLauncher import com.swordfish.lemuroid.app.shared.main.BusyActivity -import com.swordfish.lemuroid.app.shared.main.PostGameHandler +import com.swordfish.lemuroid.app.shared.main.GameLaunchTaskHandler +import com.swordfish.lemuroid.app.shared.savesync.SaveSyncWork import com.swordfish.lemuroid.app.shared.settings.SettingsInteractor import com.swordfish.lemuroid.ext.feature.review.ReviewManager import com.swordfish.lemuroid.lib.android.RetrogradeAppCompatActivity @@ -40,7 +42,8 @@ import com.swordfish.lemuroid.lib.injection.PerFragment import com.swordfish.lemuroid.lib.library.SystemID import com.swordfish.lemuroid.lib.library.db.RetrogradeDatabase import com.swordfish.lemuroid.lib.storage.DirectoriesManager -import com.swordfish.lemuroid.lib.ui.setVisibleOrGone +import com.swordfish.lemuroid.common.view.setVisibleOrGone +import com.swordfish.lemuroid.lib.savesync.SaveSyncManager import dagger.Provides import dagger.android.ContributesAndroidInjector import io.reactivex.rxkotlin.subscribeBy @@ -49,13 +52,16 @@ import javax.inject.Inject class MainActivity : RetrogradeAppCompatActivity(), BusyActivity { - @Inject lateinit var postGameHandler: PostGameHandler + @Inject lateinit var gameLaunchTaskHandler: GameLaunchTaskHandler + @Inject lateinit var saveSyncManager: SaveSyncManager private val reviewManager = ReviewManager() private var mainViewModel: MainViewModel? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + window.navigationBarColor = SurfaceColors.SURFACE_2.getColor(this) + window.statusBarColor = SurfaceColors.SURFACE_2.getColor(this) setContentView(R.layout.activity_main) initializeActivity() } @@ -96,12 +102,19 @@ class MainActivity : RetrogradeAppCompatActivity(), BusyActivity { when (requestCode) { BaseGameActivity.REQUEST_PLAY_GAME -> { - postGameHandler.handle(true, this, resultCode, data) + gameLaunchTaskHandler.handleGameFinish(true, this, resultCode, data) .subscribeBy(Timber::e) { } } } } + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + val isSupported = saveSyncManager.isSupported() + val isConfigured = saveSyncManager.isConfigured() + menu?.findItem(R.id.menu_options_sync)?.isVisible = isSupported && isConfigured + return super.onPrepareOptionsMenu(menu) + } + override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_mobile_settings, menu) return super.onCreateOptionsMenu(menu) @@ -113,6 +126,10 @@ class MainActivity : RetrogradeAppCompatActivity(), BusyActivity { displayLemuroidHelp() true } + R.id.menu_options_sync -> { + SaveSyncWork.enqueueManualWork(this) + true + } else -> super.onOptionsItemSelected(item) } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainViewModel.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainViewModel.kt index db3a2fdf53..f860301491 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainViewModel.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainViewModel.kt @@ -3,9 +3,7 @@ package com.swordfish.lemuroid.app.mobile.feature.main import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.swordfish.lemuroid.app.shared.library.LibraryIndexMonitor -import com.swordfish.lemuroid.app.shared.savesync.SaveSyncMonitor -import com.swordfish.lemuroid.app.utils.livedata.CombinedLiveData +import com.swordfish.lemuroid.app.shared.library.PendingOperationsMonitor class MainViewModel(appContext: Context) : ViewModel() { @@ -15,7 +13,5 @@ class MainViewModel(appContext: Context) : ViewModel() { } } - private val indexingInProgress = LibraryIndexMonitor(appContext).getLiveData() - private val saveSyncInProgress = SaveSyncMonitor(appContext).getLiveData() - val displayProgress = CombinedLiveData(indexingInProgress, saveSyncInProgress) { a, b -> a || b } + val displayProgress = PendingOperationsMonitor(appContext).anyOperationInProgress() } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/search/SearchFragment.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/search/SearchFragment.kt index eac6a9e57f..f11b8cfda5 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/search/SearchFragment.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/search/SearchFragment.kt @@ -15,8 +15,9 @@ import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.mobile.shared.GamesAdapter import com.swordfish.lemuroid.app.mobile.shared.RecyclerViewFragment import com.swordfish.lemuroid.app.shared.GameInteractor +import com.swordfish.lemuroid.app.shared.covers.CoverLoader import com.swordfish.lemuroid.lib.library.db.RetrogradeDatabase -import com.swordfish.lemuroid.lib.ui.setVisibleOrGone +import com.swordfish.lemuroid.common.view.setVisibleOrGone import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDispose @@ -29,6 +30,7 @@ class SearchFragment : RecyclerViewFragment() { @Inject lateinit var retrogradeDb: RetrogradeDatabase @Inject lateinit var gameInteractor: GameInteractor + @Inject lateinit var coverLoader: CoverLoader private lateinit var searchViewModel: SearchViewModel @@ -51,7 +53,7 @@ class SearchFragment : RecyclerViewFragment() { searchViewModel = ViewModelProvider(this, SearchViewModel.Factory(retrogradeDb)) .get(SearchViewModel::class.java) - val gamesAdapter = GamesAdapter(R.layout.layout_game_list, gameInteractor) + val gamesAdapter = GamesAdapter(R.layout.layout_game_list, gameInteractor, coverLoader) searchViewModel.searchResults.cachedIn(lifecycle).observe(viewLifecycleOwner) { gamesAdapter.submitData(lifecycle, it) } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/AdvancedSettingsFragment.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/AdvancedSettingsFragment.kt index 4d07304e37..349e36f341 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/AdvancedSettingsFragment.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/AdvancedSettingsFragment.kt @@ -6,6 +6,7 @@ import androidx.appcompat.app.AlertDialog import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.swordfish.lemuroid.R +import com.swordfish.lemuroid.app.shared.settings.AdvancedSettingsPreferences import com.swordfish.lemuroid.app.shared.settings.SettingsInteractor import com.swordfish.lemuroid.lib.preferences.SharedPreferencesHelper import dagger.android.support.AndroidSupportInjection @@ -29,6 +30,7 @@ class AdvancedSettingsFragment : PreferenceFragmentCompat() { preferenceManager.preferenceDataStore = SharedPreferencesHelper.getSharedPreferencesDataStore(requireContext()) setPreferencesFromResource(R.xml.mobile_settings_advanced, rootKey) + AdvancedSettingsPreferences.updateCachePreferences(preferenceScreen) } override fun onPreferenceTreeClick(preference: Preference?): Boolean { diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/GamepadSettingsFragment.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/GamepadSettingsFragment.kt index 50fea6547a..f8415c668e 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/GamepadSettingsFragment.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/GamepadSettingsFragment.kt @@ -6,7 +6,7 @@ import android.view.InputDevice import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.swordfish.lemuroid.R -import com.swordfish.lemuroid.app.shared.settings.GamePadManager +import com.swordfish.lemuroid.app.shared.input.InputDeviceManager import com.swordfish.lemuroid.app.shared.settings.GamePadPreferencesHelper import com.swordfish.lemuroid.lib.preferences.SharedPreferencesHelper import com.uber.autodispose.android.lifecycle.scope @@ -18,7 +18,7 @@ import javax.inject.Inject class GamepadSettingsFragment : PreferenceFragmentCompat() { @Inject lateinit var gamePadPreferencesHelper: GamePadPreferencesHelper - @Inject lateinit var gamePadManager: GamePadManager + @Inject lateinit var inputDeviceManager: InputDeviceManager override fun onAttach(context: Context) { AndroidSupportInjection.inject(this) @@ -29,7 +29,7 @@ class GamepadSettingsFragment : PreferenceFragmentCompat() { preferenceManager.preferenceDataStore = SharedPreferencesHelper.getSharedPreferencesDataStore(requireContext()) setPreferencesFromResource(R.xml.empty_preference_screen, rootKey) - gamePadManager.getGamePadsObservable() + inputDeviceManager.getGamePadsObservable() .distinctUntilChanged() .observeOn(AndroidSchedulers.mainThread()) .autoDispose(scope()) diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/RxSettingsManager.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/RxSettingsManager.kt index 92c5156a83..788409df23 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/RxSettingsManager.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/RxSettingsManager.kt @@ -4,15 +4,14 @@ import android.content.Context import android.content.SharedPreferences import com.f2prateek.rx.preferences2.RxSharedPreferences import com.swordfish.lemuroid.R +import com.swordfish.lemuroid.lib.storage.cache.CacheCleaner import dagger.Lazy import io.reactivex.Single +import io.reactivex.rxkotlin.Singles import io.reactivex.schedulers.Schedulers import kotlin.math.roundToInt -class RxSettingsManager( - private val context: Context, - sharedPreferences: Lazy -) { +class RxSettingsManager(private val context: Context, sharedPreferences: Lazy) { private val rxSharedPreferences = Single.fromCallable { RxSharedPreferences.create(sharedPreferences.get()) @@ -39,6 +38,17 @@ class RxSettingsManager( val syncStatesCores = stringSetPreference(R.string.pref_key_save_sync_cores, setOf()) + val enableRumble = booleanPreference(R.string.pref_key_enable_rumble, false) + + val enableDeviceRumble = booleanPreference(R.string.pref_key_enable_device_rumble, false) + + val cacheSizeBytes = stringPreference( + R.string.pref_key_max_cache_size, + Single.fromCallable { CacheCleaner.getDefaultCacheLimit().toString() } + ) + + val allowDirectGameLoad = booleanPreference(R.string.pref_key_allow_direct_game_load, true) + private fun booleanPreference(keyId: Int, default: Boolean): Single { return rxSharedPreferences.flatMap { it.getBoolean(getString(keyId), default) @@ -49,11 +59,15 @@ class RxSettingsManager( } private fun stringPreference(keyId: Int, default: String): Single { - return rxSharedPreferences.flatMap { - it.getString(getString(keyId), default) + return stringPreference(keyId, Single.just(default)) + } + + private fun stringPreference(keyId: Int, default: Single): Single { + return Singles.zip(rxSharedPreferences, default).flatMap { (preferences, defaultValue) -> + preferences.getString(getString(keyId), defaultValue) .asObservable() .subscribeOn(Schedulers.io()) - .first(default) + .first(defaultValue) } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/SaveSyncFragment.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/SaveSyncFragment.kt index 84b6af9f43..57d5f623ff 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/SaveSyncFragment.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/SaveSyncFragment.kt @@ -5,7 +5,7 @@ import android.os.Bundle import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.swordfish.lemuroid.R -import com.swordfish.lemuroid.app.shared.savesync.SaveSyncMonitor +import com.swordfish.lemuroid.app.shared.library.PendingOperationsMonitor import com.swordfish.lemuroid.app.shared.settings.SaveSyncPreferences import com.swordfish.lemuroid.lib.preferences.SharedPreferencesHelper import com.swordfish.lemuroid.lib.savesync.SaveSyncManager @@ -43,7 +43,7 @@ class SaveSyncFragment : PreferenceFragmentCompat() { override fun onResume() { super.onResume() saveSyncPreferences.updatePreferences(preferenceScreen, false) - SaveSyncMonitor(requireContext()).getLiveData().observe(this) { + PendingOperationsMonitor(requireContext()).anySaveOperationInProgress().observe(this) { saveSyncPreferences.updatePreferences(preferenceScreen, it) } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/SettingsFragment.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/SettingsFragment.kt index 9d628f3bfb..9ccf5fbda1 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/SettingsFragment.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/SettingsFragment.kt @@ -63,6 +63,7 @@ class SettingsFragment : PreferenceFragmentCompat() { val currentDirectory: Preference? = findPreference(getString(R.string.pref_key_extenral_folder)) val rescanPreference: Preference? = findPreference(getString(R.string.pref_key_rescan)) + val stopRescanPreference: Preference? = findPreference(getString(R.string.pref_key_stop_rescan)) val displayBiosPreference: Preference? = findPreference(getString(R.string.pref_key_display_bios_info)) val resetSettings: Preference? = findPreference(getString(R.string.pref_key_reset_settings)) @@ -79,6 +80,11 @@ class SettingsFragment : PreferenceFragmentCompat() { displayBiosPreference?.isEnabled = !it resetSettings?.isEnabled = !it } + + settingsViewModel.directoryScanInProgress.observe(this) { + stopRescanPreference?.isVisible = it + rescanPreference?.isVisible = !it + } } private fun getDisplayNameForFolderUri(uri: Uri) = DocumentFile.fromTreeUri(requireContext(), uri)?.name @@ -86,6 +92,7 @@ class SettingsFragment : PreferenceFragmentCompat() { override fun onPreferenceTreeClick(preference: Preference?): Boolean { when (preference?.key) { getString(R.string.pref_key_rescan) -> rescanLibrary() + getString(R.string.pref_key_stop_rescan) -> stopRescanLibrary() getString(R.string.pref_key_extenral_folder) -> handleChangeExternalFolder() getString(R.string.pref_key_open_gamepad_settings) -> handleOpenGamePadSettings() getString(R.string.pref_key_open_save_sync_settings) -> handleDisplaySaveSync() @@ -139,7 +146,11 @@ class SettingsFragment : PreferenceFragmentCompat() { } private fun rescanLibrary() { - context?.let { LibraryIndexScheduler.scheduleFullSync(it) } + context?.let { LibraryIndexScheduler.scheduleLibrarySync(it) } + } + + private fun stopRescanLibrary() { + context?.let { LibraryIndexScheduler.cancelLibrarySync(it) } } @dagger.Module diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/SettingsViewModel.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/SettingsViewModel.kt index 22ba8f6db1..e0ca22d817 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/SettingsViewModel.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/SettingsViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.f2prateek.rx.preferences2.RxSharedPreferences import com.swordfish.lemuroid.R -import com.swordfish.lemuroid.app.shared.library.LibraryIndexMonitor +import com.swordfish.lemuroid.app.shared.library.PendingOperationsMonitor class SettingsViewModel( context: Context, @@ -28,5 +28,7 @@ class SettingsViewModel( .asObservable() .filter { it.isNotBlank() } - val indexingInProgress = LibraryIndexMonitor(context).getLiveData() + val indexingInProgress = PendingOperationsMonitor(context).anyLibraryOperationInProgress() + + val directoryScanInProgress = PendingOperationsMonitor(context).isDirectoryScanInProgress() } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/systems/MetaSystemsFragment.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/systems/MetaSystemsFragment.kt index e96255cedb..413b974288 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/systems/MetaSystemsFragment.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/systems/MetaSystemsFragment.kt @@ -10,7 +10,7 @@ import com.swordfish.lemuroid.app.mobile.shared.GridSpaceDecoration import com.swordfish.lemuroid.app.mobile.shared.RecyclerViewFragment import com.swordfish.lemuroid.lib.library.MetaSystemID import com.swordfish.lemuroid.lib.library.db.RetrogradeDatabase -import com.swordfish.lemuroid.lib.ui.setVisibleOrGone +import com.swordfish.lemuroid.common.view.setVisibleOrGone import com.swordfish.lemuroid.lib.util.subscribeBy import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider import com.uber.autodispose.autoDispose @@ -29,7 +29,10 @@ class MetaSystemsFragment : RecyclerViewFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - metaSystemsViewModel = ViewModelProvider(this, MetaSystemsViewModel.Factory(retrogradeDb)) + metaSystemsViewModel = ViewModelProvider( + this, + MetaSystemsViewModel.Factory(retrogradeDb, requireContext().applicationContext) + ) .get(MetaSystemsViewModel::class.java) metaSystemsAdapter = MetaSystemsAdapter { navigateToGames(it) } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/systems/MetaSystemsViewModel.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/systems/MetaSystemsViewModel.kt index 89df810cf0..1b9508af8c 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/systems/MetaSystemsViewModel.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/systems/MetaSystemsViewModel.kt @@ -1,5 +1,6 @@ package com.swordfish.lemuroid.app.mobile.feature.systems +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.swordfish.lemuroid.app.shared.systems.MetaSystemInfo @@ -8,20 +9,26 @@ import com.swordfish.lemuroid.lib.library.db.RetrogradeDatabase import com.swordfish.lemuroid.lib.library.metaSystemID import io.reactivex.Observable -class MetaSystemsViewModel(retrogradeDb: RetrogradeDatabase) : ViewModel() { +class MetaSystemsViewModel(retrogradeDb: RetrogradeDatabase, appContext: Context) : ViewModel() { - class Factory(val retrogradeDb: RetrogradeDatabase) : ViewModelProvider.Factory { + class Factory( + val retrogradeDb: RetrogradeDatabase, + val appContext: Context + ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return MetaSystemsViewModel(retrogradeDb) as T + return MetaSystemsViewModel(retrogradeDb, appContext) as T } } val availableMetaSystems: Observable> = retrogradeDb.gameDao() .selectSystemsWithCount() .map { systemCounts -> - systemCounts.filter { (_, count) -> count > 0 } + systemCounts.asSequence() + .filter { (_, count) -> count > 0 } .map { (systemId, count) -> GameSystem.findById(systemId).metaSystemID() to count } .groupBy { (metaSystemId, _) -> metaSystemId } .map { (metaSystemId, counts) -> MetaSystemInfo(metaSystemId, counts.sumBy { it.second }) } + .sortedBy { it.getName(appContext) } + .toList() } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/shared/GamesAdapter.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/shared/GamesAdapter.kt index 69b2bcb2fb..708c8d802e 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/shared/GamesAdapter.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/shared/GamesAdapter.kt @@ -28,12 +28,12 @@ class GameViewHolder(parent: View) : RecyclerView.ViewHolder(parent) { favoriteToggle = itemView.findViewById(R.id.favorite_toggle) } - fun bind(game: Game, gameInteractor: GameInteractor) { + fun bind(game: Game, gameInteractor: GameInteractor, coverLoader: CoverLoader) { titleView?.text = game.title subtitleView?.text = GameUtils.getGameSubtitle(itemView.context, game) favoriteToggle?.isChecked = game.isFavorite - CoverLoader.loadCover(game, coverView) + coverLoader.loadCover(game, coverView) itemView.setOnClickListener { gameInteractor.onGamePlay(game) } itemView.setOnCreateContextMenuListener(GameContextMenuListener(gameInteractor, game)) @@ -43,9 +43,9 @@ class GameViewHolder(parent: View) : RecyclerView.ViewHolder(parent) { } } - fun unbind() { + fun unbind(coverLoader: CoverLoader) { coverView?.apply { - CoverLoader.cancelRequest(this) + coverLoader.cancelRequest(this) this.setImageDrawable(null) } itemView.setOnClickListener(null) @@ -56,7 +56,8 @@ class GameViewHolder(parent: View) : RecyclerView.ViewHolder(parent) { class GamesAdapter( private val baseLayout: Int, - private val gameInteractor: GameInteractor + private val gameInteractor: GameInteractor, + private val coverLoader: CoverLoader ) : PagingDataAdapter(Game.DIFF_CALLBACK) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { @@ -64,10 +65,10 @@ class GamesAdapter( } override fun onBindViewHolder(holder: GameViewHolder, position: Int) { - getItem(position)?.let { holder.bind(it, gameInteractor) } + getItem(position)?.let { holder.bind(it, gameInteractor, coverLoader) } } override fun onViewRecycled(holder: GameViewHolder) { - holder.unbind() + holder.unbind(coverLoader) } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/shared/NotificationsManager.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/shared/NotificationsManager.kt index 4d4b4b8344..90fad1d601 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/shared/NotificationsManager.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/shared/NotificationsManager.kt @@ -11,6 +11,7 @@ import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.mobile.feature.game.GameActivity +import com.swordfish.lemuroid.app.shared.library.LibraryIndexBroadcastReceiver import com.swordfish.lemuroid.lib.library.db.entity.Game class NotificationsManager(private val applicationContext: Context) { @@ -19,7 +20,12 @@ class NotificationsManager(private val applicationContext: Context) { createDefaultNotificationChannel() val intent = Intent(applicationContext, GameActivity::class.java) - val contentIntent = PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) + val contentIntent = PendingIntent.getActivity( + applicationContext, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) val title = game?.let { applicationContext.getString(R.string.game_running_notification_title, game.title) @@ -41,12 +47,27 @@ class NotificationsManager(private val applicationContext: Context) { fun libraryIndexingNotification(): Notification { createDefaultNotificationChannel() + val broadcastIntent = Intent(applicationContext, LibraryIndexBroadcastReceiver::class.java) + val broadcastPendingIntent: PendingIntent = PendingIntent.getBroadcast( + applicationContext, + 0, + broadcastIntent, + PendingIntent.FLAG_IMMUTABLE + ) + val builder = NotificationCompat.Builder(applicationContext, DEFAULT_CHANNEL_ID) .setSmallIcon(R.drawable.ic_lemuroid_tiny) .setContentTitle(applicationContext.getString(R.string.library_index_notification_title)) .setContentText(applicationContext.getString(R.string.library_index_notification_message)) .setProgress(100, 0, true) .setPriority(NotificationCompat.PRIORITY_LOW) + .addAction( + NotificationCompat.Action( + null, + applicationContext.getString(R.string.library_index_notification_action_cancel), + broadcastPendingIntent + ) + ) return builder.build() } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/covers/CoverLoader.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/covers/CoverLoader.kt index 016c464809..7ed744fae5 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/covers/CoverLoader.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/covers/CoverLoader.kt @@ -1,51 +1,65 @@ package com.swordfish.lemuroid.app.shared.covers +import android.content.Context import android.widget.ImageView +import coil.ImageLoader import coil.load +import coil.util.CoilUtils import com.swordfish.lemuroid.common.drawable.TextDrawable import com.swordfish.lemuroid.common.graphics.ColorUtils import com.swordfish.lemuroid.lib.library.db.entity.Game +import okhttp3.OkHttpClient -object CoverLoader { +class CoverLoader(applicationContext: Context) { + + private val imageLoader = ImageLoader.Builder(applicationContext) + .crossfade(true) + .okHttpClient { + OkHttpClient.Builder() + .cache(CoilUtils.createDefaultCache(applicationContext)) + .addNetworkInterceptor(ThrottleFailedThumbnailsInterceptor) + .build() + } + .build() fun loadCover(game: Game, imageView: ImageView?) { if (imageView == null) return - imageView.load(game.coverFrontUrl) { - crossfade(true) - + imageView.load(game.coverFrontUrl, imageLoader) { val fallbackDrawable = getFallbackDrawable(game) fallback(fallbackDrawable) error(fallbackDrawable) } } - fun getFallbackRemoteUrl(game: Game): String { - val color = Integer.toHexString(computeColor(game)).substring(2) - val title = computeTitle(game) - return "https://fakeimg.pl/512x512/$color/fff/?font=bebas&text=$title" + fun cancelRequest(imageView: ImageView) { + // coil-kt automatically does that for us. } - fun getFallbackDrawable(game: Game) = - TextDrawable(computeTitle(game), computeColor(game)) + companion object { + fun getFallbackDrawable(game: Game) = + TextDrawable(computeTitle(game), computeColor(game)) - private fun computeTitle(game: Game): String { - val sanitizedName = game.title - .replace(Regex("\\(.*\\)"), "") + fun getFallbackRemoteUrl(game: Game): String { + val color = Integer.toHexString(computeColor(game)).substring(2) + val title = computeTitle(game) + return "https://fakeimg.pl/512x512/$color/fff/?font=bebas&text=$title" + } - return sanitizedName.asSequence() - .filter { it.isDigit() or it.isUpperCase() or (it == '&') } - .take(3) - .joinToString("") - .ifBlank { game.title.first().toString() } - .capitalize() - } + private fun computeTitle(game: Game): String { + val sanitizedName = game.title + .replace(Regex("\\(.*\\)"), "") - private fun computeColor(game: Game): Int { - return ColorUtils.randomColor(game.title) - } + return sanitizedName.asSequence() + .filter { it.isDigit() or it.isUpperCase() or (it == '&') } + .take(3) + .joinToString("") + .ifBlank { game.title.first().toString() } + .capitalize() + } - fun cancelRequest(imageView: ImageView) { - // coil-kt automatically does that for us. + private fun computeColor(game: Game): Int { + return ColorUtils.randomColor(game.title) + } } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/covers/ThrottleFailedThumbnailsInterceptor.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/covers/ThrottleFailedThumbnailsInterceptor.kt new file mode 100644 index 0000000000..9e549e3c82 --- /dev/null +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/covers/ThrottleFailedThumbnailsInterceptor.kt @@ -0,0 +1,26 @@ +package com.swordfish.lemuroid.app.shared.covers + +import android.util.LruCache +import okhttp3.Interceptor +import okhttp3.Response +import java.io.IOException + +object ThrottleFailedThumbnailsInterceptor : Interceptor { + + private val failedThumbnailsStatusCode = LruCache(256 * 1024) + + override fun intercept(chain: Interceptor.Chain): Response { + val requestUrl = chain.request().url.toString() + val previousFailure = failedThumbnailsStatusCode[requestUrl] + if (previousFailure != null) { + throw IOException("Thumbnail previously failed with code: $previousFailure") + } + + val response = chain.proceed(chain.request()) + if (!response.isSuccessful) { + failedThumbnailsStatusCode.put(chain.request().url.toString(), response.code) + } + + return response + } +} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/game/BaseGameActivity.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/game/BaseGameActivity.kt index cabd562234..d7ca81cb08 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/game/BaseGameActivity.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/game/BaseGameActivity.kt @@ -25,21 +25,18 @@ import com.swordfish.lemuroid.app.mobile.feature.game.GameActivity import com.swordfish.lemuroid.app.mobile.feature.settings.RxSettingsManager import com.swordfish.lemuroid.app.shared.GameMenuContract import com.swordfish.lemuroid.app.shared.ImmersiveActivity +import com.swordfish.lemuroid.app.shared.rumble.RumbleManager import com.swordfish.lemuroid.app.shared.coreoptions.CoreOption import com.swordfish.lemuroid.app.shared.coreoptions.LemuroidCoreOption -import com.swordfish.lemuroid.app.shared.savesync.SaveSyncWork import com.swordfish.lemuroid.app.shared.settings.ControllerConfigsManager -import com.swordfish.lemuroid.app.shared.settings.GamePadManager +import com.swordfish.lemuroid.app.shared.input.InputDeviceManager +import com.swordfish.lemuroid.app.shared.input.getInputClass import com.swordfish.lemuroid.app.tv.game.TVGameActivity import com.swordfish.lemuroid.common.animationDuration import com.swordfish.lemuroid.common.displayToast import com.swordfish.lemuroid.common.dump import com.swordfish.lemuroid.common.graphics.GraphicsUtils -import com.swordfish.lemuroid.common.graphics.takeScreenshot import com.swordfish.lemuroid.common.kotlin.NTuple4 -import com.swordfish.lemuroid.common.kotlin.filterNotNullValues -import com.swordfish.lemuroid.common.kotlin.toIndexedMap -import com.swordfish.lemuroid.common.kotlin.zipOnKeys import com.swordfish.lemuroid.common.rx.BehaviorRelayNullableProperty import com.swordfish.lemuroid.common.rx.BehaviorRelayProperty import com.swordfish.lemuroid.common.rx.RXUtils @@ -60,8 +57,13 @@ import com.swordfish.lemuroid.lib.saves.SavesManager import com.swordfish.lemuroid.lib.saves.StatesManager import com.swordfish.lemuroid.lib.saves.StatesPreviewManager import com.swordfish.lemuroid.lib.storage.RomFiles -import com.swordfish.lemuroid.lib.storage.cache.CacheCleanerWork -import com.swordfish.lemuroid.lib.ui.setVisibleOrGone +import com.swordfish.lemuroid.common.graphics.takeScreenshot +import com.swordfish.lemuroid.common.kotlin.NTuple5 +import com.swordfish.lemuroid.common.kotlin.filterNotNullValues +import com.swordfish.lemuroid.common.kotlin.toIndexedMap +import com.swordfish.lemuroid.common.kotlin.zipOnKeys +import com.swordfish.lemuroid.common.longAnimationDuration +import com.swordfish.lemuroid.common.view.setVisibleOrGone import com.swordfish.lemuroid.lib.util.subscribeBy import com.swordfish.libretrodroid.Controller import com.swordfish.libretrodroid.GLRetroView @@ -109,9 +111,10 @@ abstract class BaseGameActivity : ImmersiveActivity() { @Inject lateinit var statesPreviewManager: StatesPreviewManager @Inject lateinit var savesManager: SavesManager @Inject lateinit var coreVariablesManager: CoreVariablesManager - @Inject lateinit var gamePadManager: GamePadManager + @Inject lateinit var inputDeviceManager: InputDeviceManager @Inject lateinit var gameLoader: GameLoader @Inject lateinit var controllerConfigsManager: ControllerConfigsManager + @Inject lateinit var rumbleManager: RumbleManager private var defaultExceptionHandler: Thread.UncaughtExceptionHandler? = Thread.getDefaultUncaughtExceptionHandler() @@ -168,6 +171,15 @@ abstract class BaseGameActivity : ImmersiveActivity() { } } + private fun initializeRumble() { + retroGameViewMaybe() + .flatMapCompletable { + rumbleManager.processRumbleEvents(systemCoreConfig, it.getRumbleEvents()) + } + .autoDispose(scope()) + .subscribeBy(Timber::e) { } + } + private fun setUpExceptionsHandler() { Thread.setDefaultUncaughtExceptionHandler { thread, exception -> performUnsuccessfulActivityFinish(exception) @@ -211,6 +223,7 @@ abstract class BaseGameActivity : ImmersiveActivity() { gameData: GameLoader.GameData, screenFilter: String, lowLatencyAudio: Boolean, + enableRumble: Boolean ): GLRetroView { val data = GLRetroViewData(this).apply { coreFilePath = gameData.coreLibrary @@ -231,6 +244,8 @@ abstract class BaseGameActivity : ImmersiveActivity() { saveRAMState = gameData.saveRAMData shader = getShaderForSystem(screenFilter, system) preferLowLatencyAudio = lowLatencyAudio + rumbleEventsEnabled = enableRumble + skipDuplicateFrames = systemCoreConfig.skipDuplicateFrames } val retroGameView = GLRetroView(this, data) @@ -366,6 +381,7 @@ abstract class BaseGameActivity : ImmersiveActivity() { SystemID.NGC -> GLRetroView.SHADER_LCD SystemID.WS -> GLRetroView.SHADER_LCD SystemID.WSC -> GLRetroView.SHADER_LCD + SystemID.NINTENDO_3DS -> GLRetroView.SHADER_LCD } } } @@ -419,6 +435,8 @@ abstract class BaseGameActivity : ImmersiveActivity() { .subscribeBy(Timber::e) { controllerConfigs = it } + + initializeRumble() } private fun getCoreOptions(): List { @@ -454,7 +472,7 @@ abstract class BaseGameActivity : ImmersiveActivity() { } private fun setupGamePadShortcuts() { - gamePadManager.getGamePadMenuShortCutObservable() + inputDeviceManager.getInputMenuShortCutObservable() .distinctUntilChanged() .observeOn(AndroidSchedulers.mainThread()) .autoDispose(scope()) @@ -469,7 +487,7 @@ abstract class BaseGameActivity : ImmersiveActivity() { private fun setupGamePadMotions() { val events = Observables.combineLatest( - gamePadManager.getGamePadsPortMapperObservable(), + inputDeviceManager.getGamePadsPortMapperObservable(), motionEventsSubjects ).share() @@ -484,7 +502,7 @@ abstract class BaseGameActivity : ImmersiveActivity() { events .flatMap { (ports, event) -> val port = ports(event.device) - val axes = GamePadManager.TRIGGER_MOTIONS_TO_KEYS.entries + val axes = event.device.getInputClass().getAxesMap().entries Observable.fromIterable(axes).map { (axis, button) -> val action = if (event.getAxisValue(axis) > 0.5) { KeyEvent.ACTION_DOWN @@ -511,16 +529,17 @@ abstract class BaseGameActivity : ImmersiveActivity() { val pressedKeys = mutableSetOf() val filteredKeyEvents = keyEventsSubjects + .filter { it.repeatCount == 0 } .map { Triple(it.device, it.action, it.keyCode) } .distinctUntilChanged() - val shortcutKeys = gamePadManager.getGamePadMenuShortCutObservable() + val shortcutKeys = inputDeviceManager.getInputMenuShortCutObservable() .map { it.toNullable()?.keys ?: setOf() } val combinedObservable = RXUtils.combineLatest( shortcutKeys, - gamePadManager.getGamePadsPortMapperObservable(), - gamePadManager.getGamePadsBindingsObservable(), + inputDeviceManager.getGamePadsPortMapperObservable(), + inputDeviceManager.getInputBindingsObservable(), filteredKeyEvents ) @@ -659,7 +678,7 @@ abstract class BaseGameActivity : ImmersiveActivity() { } override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { - if (event != null && keyCode in GamePadManager.INPUT_KEYS) { + if (event != null && keyCode in event.device.getInputClass().getInputKeys()) { keyEventsSubjects.accept(event) return true } @@ -667,7 +686,7 @@ abstract class BaseGameActivity : ImmersiveActivity() { } override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { - if (event != null && keyCode in GamePadManager.INPUT_KEYS) { + if (event != null && keyCode in event.device.getInputClass().getInputKeys()) { keyEventsSubjects.accept(event) return true } @@ -703,8 +722,6 @@ abstract class BaseGameActivity : ImmersiveActivity() { putExtra(PLAY_GAME_RESULT_LEANBACK, intent.getBooleanExtra(EXTRA_LEANBACK, false)) } - rescheduleBackgroundWork() - setResult(Activity.RESULT_OK, resultIntent) finishAndExitProcess() @@ -732,18 +749,6 @@ abstract class BaseGameActivity : ImmersiveActivity() { open fun onFinishTriggered() { } - private fun cancelBackgroundWork() { - SaveSyncWork.cancelAutoWork(applicationContext) - SaveSyncWork.cancelManualWork(applicationContext) - CacheCleanerWork.cancelCleanCacheLRU(applicationContext) - } - - private fun rescheduleBackgroundWork() { - // Let's slightly delay the sync. Maybe the user wants to play another game. - SaveSyncWork.enqueueAutoWork(applicationContext, 5) - CacheCleanerWork.enqueueCleanCacheLRU(applicationContext) - } - private fun getAutoSaveCompletable(game: Game): Completable { return isAutoSaveEnabled() .filter { it } @@ -831,8 +836,11 @@ abstract class BaseGameActivity : ImmersiveActivity() { } } - private fun reset() { - retroGameView?.reset() + private fun reset(): Completable { + return Completable.timer(longAnimationDuration().toLong(), TimeUnit.MILLISECONDS) + .doOnSubscribe { loading = true } + .doAfterTerminate { loading = false } + .doOnComplete { retroGameView?.reset() } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -841,6 +849,10 @@ abstract class BaseGameActivity : ImmersiveActivity() { Timber.i("Game menu dialog response: ${data?.extras.dump()}") if (data?.getBooleanExtra(GameMenuContract.RESULT_RESET, false) == true) { reset() + .autoDispose(scope()) + .subscribeBy(Timber::e) { + retroGameView?.reset() + } } if (data?.hasExtra(GameMenuContract.RESULT_SAVE) == true) { saveSlot(data.getIntExtra(GameMenuContract.RESULT_SAVE, 0)) @@ -882,27 +894,38 @@ abstract class BaseGameActivity : ImmersiveActivity() { private fun loadGame() { val requestLoadSave = intent.getBooleanExtra(EXTRA_LOAD_SAVE, false) - cancelBackgroundWork() - setupLoadingView() - Singles.zip(settingsManager.autoSave, settingsManager.screenFilter, settingsManager.lowLatencyAudio, ::Triple) - .flatMapObservable { (autoSaveEnabled, filter, lowLatencyAudio) -> + Singles.zip( + settingsManager.autoSave, + settingsManager.screenFilter, + settingsManager.lowLatencyAudio, + settingsManager.enableRumble, + settingsManager.allowDirectGameLoad, + ::NTuple5 + ) + .flatMapObservable { (autoSaveEnabled, filter, lowLatencyAudio, enableRumble, directLoad) -> gameLoader.load( applicationContext, game, requestLoadSave && autoSaveEnabled, - systemCoreConfig - ).map { Triple(it, filter, lowLatencyAudio) } + systemCoreConfig, + directLoad + ).map { NTuple4(it, filter, lowLatencyAudio, enableRumble) } } .subscribeOn(Schedulers.single()) .observeOn(AndroidSchedulers.mainThread()) .autoDispose(scope()) .subscribe( - { (loadingState, filter, lowLatencyAudio) -> + { (loadingState, filter, lowLatencyAudio, enableRumble) -> displayLoadingState(loadingState) if (loadingState is GameLoader.LoadingState.Ready) { - retroGameView = initializeRetroGameView(loadingState.gameData, filter, lowLatencyAudio) + retroGameView = initializeRetroGameView( + loadingState.gameData, + filter, + lowLatencyAudio, + systemCoreConfig.rumbleSupported && enableRumble + ) } }, { diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/game/ExternalGameLauncherActivity.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/game/ExternalGameLauncherActivity.kt index 1d8e8b98d7..2a4f234f3a 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/game/ExternalGameLauncherActivity.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/game/ExternalGameLauncherActivity.kt @@ -4,26 +4,22 @@ import android.content.Intent import android.os.Bundle import android.view.View import androidx.lifecycle.LiveData -import androidx.lifecycle.LiveDataReactiveStreams import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.shared.ImmersiveActivity -import com.swordfish.lemuroid.app.shared.library.LibraryIndexMonitor -import com.swordfish.lemuroid.app.shared.main.PostGameHandler -import com.swordfish.lemuroid.app.shared.savesync.SaveSyncMonitor +import com.swordfish.lemuroid.app.shared.library.PendingOperationsMonitor +import com.swordfish.lemuroid.app.shared.main.GameLaunchTaskHandler import com.swordfish.lemuroid.app.tv.channel.ChannelUpdateWork import com.swordfish.lemuroid.app.tv.shared.TVHelper import com.swordfish.lemuroid.app.utils.android.displayErrorDialog -import com.swordfish.lemuroid.app.utils.livedata.CombinedLiveData +import com.swordfish.lemuroid.app.utils.livedata.toObservable import com.swordfish.lemuroid.common.animationDuration import com.swordfish.lemuroid.lib.core.CoresSelection -import com.swordfish.lemuroid.lib.library.GameSystem import com.swordfish.lemuroid.lib.library.db.RetrogradeDatabase -import com.swordfish.lemuroid.lib.ui.setVisibleOrGone +import com.swordfish.lemuroid.common.view.setVisibleOrGone import com.swordfish.lemuroid.lib.util.subscribeBy import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDispose import io.reactivex.Completable -import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers @@ -40,8 +36,9 @@ import javax.inject.Inject class ExternalGameLauncherActivity : ImmersiveActivity() { @Inject lateinit var retrogradeDatabase: RetrogradeDatabase - @Inject lateinit var postGameHandler: PostGameHandler + @Inject lateinit var gameLaunchTaskHandler: GameLaunchTaskHandler @Inject lateinit var coresSelection: CoresSelection + @Inject lateinit var gameLauncher: GameLauncher override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -51,20 +48,16 @@ class ExternalGameLauncherActivity : ImmersiveActivity() { val gameId = intent.data?.pathSegments?.let { it[it.size - 1].toInt() }!! - val publisher = LiveDataReactiveStreams.toPublisher(this, getLoadingLiveData()) - val loadingSubject = BehaviorSubject.createDefault(true) - Observable.fromPublisher(publisher) + getLoadingLiveData() + .toObservable(this) .filter { !it } .firstElement() - .flatMapSingle { + .flatMap { retrogradeDatabase.gameDao() - .selectById(gameId).subscribeOn(Schedulers.io()) - .flatMapSingle { game -> - coresSelection.getCoreConfigForSystem(GameSystem.findById(game.systemId)) - .map { game to it } - } + .selectById(gameId) + .subscribeOn(Schedulers.io()) } .subscribeOn(Schedulers.io()) .delay(animationDuration().toLong(), TimeUnit.MILLISECONDS) @@ -74,10 +67,10 @@ class ExternalGameLauncherActivity : ImmersiveActivity() { .autoDispose(scope()) .subscribeBy( { displayErrorMessage() }, - { (game, systemCoreConfig) -> - BaseGameActivity.launchGame( + { }, + { game -> + gameLauncher.launchGameAsync( this, - systemCoreConfig, game, true, TVHelper.isTV(applicationContext) @@ -100,12 +93,7 @@ class ExternalGameLauncherActivity : ImmersiveActivity() { } private fun getLoadingLiveData(): LiveData { - return CombinedLiveData( - LibraryIndexMonitor(applicationContext).getLiveData(), - SaveSyncMonitor(applicationContext).getLiveData() - ) { libraryIndex, saveSync -> - libraryIndex || saveSync - } + return PendingOperationsMonitor(applicationContext).anyOperationInProgress() } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -121,9 +109,10 @@ class ExternalGameLauncherActivity : ImmersiveActivity() { Completable.complete() } - postGameHandler.handle(false, this, resultCode, data) - .andThen { updateChannelCallback } - .doAfterTerminate { finish() } + gameLaunchTaskHandler.handleGameFinish(false, this, resultCode, data) + .andThen(updateChannelCallback) + .observeOn(AndroidSchedulers.mainThread()) + .doFinally { finish() } .subscribeBy(Timber::e) { } } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/game/GameLauncher.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/game/GameLauncher.kt index 0d1913d556..e7f2ee9f75 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/game/GameLauncher.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/game/GameLauncher.kt @@ -1,19 +1,24 @@ package com.swordfish.lemuroid.app.shared.game import android.app.Activity +import com.swordfish.lemuroid.app.shared.main.GameLaunchTaskHandler import com.swordfish.lemuroid.lib.core.CoresSelection import com.swordfish.lemuroid.lib.library.GameSystem import com.swordfish.lemuroid.lib.library.db.entity.Game import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.rxkotlin.subscribeBy -class GameLauncher(private val coresSelection: CoresSelection) { +class GameLauncher( + private val coresSelection: CoresSelection, + private val gameLaunchTaskHandler: GameLaunchTaskHandler +) { fun launchGameAsync(activity: Activity, game: Game, loadSave: Boolean, leanback: Boolean) { val system = GameSystem.findById(game.systemId) coresSelection.getCoreConfigForSystem(system) .observeOn(AndroidSchedulers.mainThread()) .subscribeBy { + gameLaunchTaskHandler.handleGameStart(activity.applicationContext) BaseGameActivity.launchGame(activity, it, game, loadSave, leanback) } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/gamemenu/GameMenuHelper.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/gamemenu/GameMenuHelper.kt index 89a8054f45..041565463a 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/gamemenu/GameMenuHelper.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/gamemenu/GameMenuHelper.kt @@ -35,7 +35,7 @@ object GameMenuHelper { ) { val preference = screen.findPreference(FAST_FORWARD) preference?.isChecked = fastForwardEnabled - preference?.isEnabled = fastForwardSupported + preference?.isVisible = fastForwardSupported } fun setupSaveOption( @@ -43,17 +43,17 @@ object GameMenuHelper { systemCoreConfig: SystemCoreConfig ) { val savesOption = screen.findPreference(SECTION_SAVE_GAME) - savesOption?.isEnabled = systemCoreConfig.statesSupported + savesOption?.isVisible = systemCoreConfig.statesSupported val loadOption = screen.findPreference(SECTION_LOAD_GAME) - loadOption?.isEnabled = systemCoreConfig.statesSupported + loadOption?.isVisible = systemCoreConfig.statesSupported } fun setupSettingsOption( screen: PreferenceScreen, systemCoreConfig: SystemCoreConfig ) { - screen.findPreference(SECTION_CORE_OPTIONS)?.isEnabled = sequenceOf( + screen.findPreference(SECTION_CORE_OPTIONS)?.isVisible = sequenceOf( systemCoreConfig.exposedSettings.isNotEmpty(), systemCoreConfig.exposedAdvancedSettings.isNotEmpty(), systemCoreConfig.controllerConfigs.values.any { it.size > 1 } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/input/InputClass.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/input/InputClass.kt new file mode 100644 index 0000000000..3ca9cd768c --- /dev/null +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/input/InputClass.kt @@ -0,0 +1,29 @@ +package com.swordfish.lemuroid.app.shared.input + +import android.content.Context +import android.view.InputDevice +import com.swordfish.lemuroid.app.shared.settings.GameMenuShortcut + +interface InputClass { + fun getInputKeys(): List + + fun getAxesMap(): Map + + fun getDefaultBindings(): Map + + fun isSupported(device: InputDevice): Boolean + + fun isEnabledByDefault(appContext: Context): Boolean + + fun getCustomizableKeys(): List + + fun getSupportedShortcuts(): List +} + +fun InputDevice.getInputClass(): InputClass { + return when { + (sources and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD -> InputClassGamePad + (sources and InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD -> InputClassKeyboard + else -> InputClassUnknown + } +} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/input/InputClassGamePad.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/input/InputClassGamePad.kt new file mode 100644 index 0000000000..8d82c86cf5 --- /dev/null +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/input/InputClassGamePad.kt @@ -0,0 +1,137 @@ +package com.swordfish.lemuroid.app.shared.input + +import android.content.Context +import android.view.InputDevice +import android.view.KeyEvent +import android.view.MotionEvent +import com.swordfish.lemuroid.app.shared.settings.GameMenuShortcut + +object InputClassGamePad : InputClass { + + private val MINIMAL_SUPPORTED_KEYS = intArrayOf( + KeyEvent.KEYCODE_BUTTON_A, + KeyEvent.KEYCODE_BUTTON_B, + KeyEvent.KEYCODE_BUTTON_X, + KeyEvent.KEYCODE_BUTTON_Y, + ) + + private val INPUT_KEYS = listOf( + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_UP_LEFT, + KeyEvent.KEYCODE_DPAD_UP_RIGHT, + KeyEvent.KEYCODE_DPAD_DOWN_LEFT, + KeyEvent.KEYCODE_DPAD_DOWN_RIGHT, + KeyEvent.KEYCODE_BUTTON_A, + KeyEvent.KEYCODE_BUTTON_B, + KeyEvent.KEYCODE_BUTTON_X, + KeyEvent.KEYCODE_BUTTON_Y, + KeyEvent.KEYCODE_BUTTON_START, + KeyEvent.KEYCODE_BUTTON_SELECT, + KeyEvent.KEYCODE_BUTTON_L1, + KeyEvent.KEYCODE_BUTTON_R1, + KeyEvent.KEYCODE_BUTTON_L2, + KeyEvent.KEYCODE_BUTTON_R2, + KeyEvent.KEYCODE_BUTTON_THUMBL, + KeyEvent.KEYCODE_BUTTON_THUMBR, + KeyEvent.KEYCODE_BUTTON_C, + KeyEvent.KEYCODE_BUTTON_Z, + KeyEvent.KEYCODE_BUTTON_1, + KeyEvent.KEYCODE_BUTTON_2, + KeyEvent.KEYCODE_BUTTON_3, + KeyEvent.KEYCODE_BUTTON_4, + KeyEvent.KEYCODE_BUTTON_5, + KeyEvent.KEYCODE_BUTTON_6, + KeyEvent.KEYCODE_BUTTON_7, + KeyEvent.KEYCODE_BUTTON_8, + KeyEvent.KEYCODE_BUTTON_9, + KeyEvent.KEYCODE_BUTTON_10, + KeyEvent.KEYCODE_BUTTON_11, + KeyEvent.KEYCODE_BUTTON_12, + KeyEvent.KEYCODE_BUTTON_13, + KeyEvent.KEYCODE_BUTTON_14, + KeyEvent.KEYCODE_BUTTON_15, + KeyEvent.KEYCODE_BUTTON_16, + KeyEvent.KEYCODE_BUTTON_MODE + ) + + private val CUSTOMIZABLE_KEYS = listOf( + KeyEvent.KEYCODE_BUTTON_A, + KeyEvent.KEYCODE_BUTTON_B, + KeyEvent.KEYCODE_BUTTON_X, + KeyEvent.KEYCODE_BUTTON_Y, + KeyEvent.KEYCODE_BUTTON_START, + KeyEvent.KEYCODE_BUTTON_SELECT, + KeyEvent.KEYCODE_BUTTON_L1, + KeyEvent.KEYCODE_BUTTON_R1, + KeyEvent.KEYCODE_BUTTON_L2, + KeyEvent.KEYCODE_BUTTON_R2, + KeyEvent.KEYCODE_BUTTON_THUMBL, + KeyEvent.KEYCODE_BUTTON_THUMBR, + KeyEvent.KEYCODE_BUTTON_C, + KeyEvent.KEYCODE_BUTTON_Z, + KeyEvent.KEYCODE_BUTTON_1, + KeyEvent.KEYCODE_BUTTON_2, + KeyEvent.KEYCODE_BUTTON_3, + KeyEvent.KEYCODE_BUTTON_4, + KeyEvent.KEYCODE_BUTTON_5, + KeyEvent.KEYCODE_BUTTON_6, + KeyEvent.KEYCODE_BUTTON_7, + KeyEvent.KEYCODE_BUTTON_8, + KeyEvent.KEYCODE_BUTTON_9, + KeyEvent.KEYCODE_BUTTON_10, + KeyEvent.KEYCODE_BUTTON_11, + KeyEvent.KEYCODE_BUTTON_12, + KeyEvent.KEYCODE_BUTTON_13, + KeyEvent.KEYCODE_BUTTON_14, + KeyEvent.KEYCODE_BUTTON_15, + KeyEvent.KEYCODE_BUTTON_16, + KeyEvent.KEYCODE_BUTTON_MODE + ) + + private val AXES_MAP = mapOf( + MotionEvent.AXIS_BRAKE to KeyEvent.KEYCODE_BUTTON_L2, + MotionEvent.AXIS_THROTTLE to KeyEvent.KEYCODE_BUTTON_R2, + MotionEvent.AXIS_LTRIGGER to KeyEvent.KEYCODE_BUTTON_L2, + MotionEvent.AXIS_RTRIGGER to KeyEvent.KEYCODE_BUTTON_R2 + ) + + private val DEFAULT_BINDINGS = mapOf( + KeyEvent.KEYCODE_BUTTON_A to KeyEvent.KEYCODE_BUTTON_B, + KeyEvent.KEYCODE_BUTTON_B to KeyEvent.KEYCODE_BUTTON_A, + KeyEvent.KEYCODE_BUTTON_X to KeyEvent.KEYCODE_BUTTON_Y, + KeyEvent.KEYCODE_BUTTON_Y to KeyEvent.KEYCODE_BUTTON_X + ).withDefault { if (it in InputDeviceManager.OUTPUT_KEYS) it else KeyEvent.KEYCODE_UNKNOWN } + + override fun getInputKeys() = INPUT_KEYS + + override fun getAxesMap() = AXES_MAP + + override fun getDefaultBindings() = DEFAULT_BINDINGS + + override fun isEnabledByDefault(appContext: Context): Boolean = true + + override fun getCustomizableKeys(): List = CUSTOMIZABLE_KEYS + + override fun getSupportedShortcuts(): List = listOf( + GameMenuShortcut( + "L3 + R3", + setOf(KeyEvent.KEYCODE_BUTTON_THUMBL, KeyEvent.KEYCODE_BUTTON_THUMBR) + ), + GameMenuShortcut( + "Select + Start", + setOf(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_BUTTON_SELECT) + ) + ) + + override fun isSupported(device: InputDevice): Boolean { + return sequenceOf( + device.sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD, + device.hasKeys(*MINIMAL_SUPPORTED_KEYS).all { it }, + device.isVirtual.not(), + device.controllerNumber > 0 + ).all { it } + } +} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/input/InputClassKeyboard.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/input/InputClassKeyboard.kt new file mode 100644 index 0000000000..30256b11cb --- /dev/null +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/input/InputClassKeyboard.kt @@ -0,0 +1,152 @@ +package com.swordfish.lemuroid.app.shared.input + +import android.content.Context +import android.view.InputDevice +import android.view.KeyEvent +import com.swordfish.lemuroid.app.shared.settings.GameMenuShortcut + +object InputClassKeyboard : InputClass { + + private val INPUT_KEYS = listOf( + KeyEvent.KEYCODE_Q, + KeyEvent.KEYCODE_W, + KeyEvent.KEYCODE_E, + KeyEvent.KEYCODE_R, + KeyEvent.KEYCODE_T, + KeyEvent.KEYCODE_Y, + KeyEvent.KEYCODE_U, + KeyEvent.KEYCODE_I, + KeyEvent.KEYCODE_O, + KeyEvent.KEYCODE_P, + KeyEvent.KEYCODE_A, + KeyEvent.KEYCODE_S, + KeyEvent.KEYCODE_D, + KeyEvent.KEYCODE_F, + KeyEvent.KEYCODE_G, + KeyEvent.KEYCODE_H, + KeyEvent.KEYCODE_J, + KeyEvent.KEYCODE_K, + KeyEvent.KEYCODE_L, + KeyEvent.KEYCODE_Z, + KeyEvent.KEYCODE_X, + KeyEvent.KEYCODE_C, + KeyEvent.KEYCODE_V, + KeyEvent.KEYCODE_B, + KeyEvent.KEYCODE_N, + KeyEvent.KEYCODE_M, + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.KEYCODE_ENTER, + KeyEvent.KEYCODE_SHIFT_LEFT, + KeyEvent.KEYCODE_ESCAPE, + ) + + private val MINIMAL_SUPPORTED_KEYS = listOf( + KeyEvent.KEYCODE_Q, + KeyEvent.KEYCODE_W, + KeyEvent.KEYCODE_E, + KeyEvent.KEYCODE_R, + KeyEvent.KEYCODE_T, + KeyEvent.KEYCODE_Y, + KeyEvent.KEYCODE_U, + KeyEvent.KEYCODE_I, + KeyEvent.KEYCODE_O, + KeyEvent.KEYCODE_P, + KeyEvent.KEYCODE_A, + KeyEvent.KEYCODE_S, + KeyEvent.KEYCODE_D, + KeyEvent.KEYCODE_F, + KeyEvent.KEYCODE_G, + KeyEvent.KEYCODE_H, + KeyEvent.KEYCODE_J, + KeyEvent.KEYCODE_K, + KeyEvent.KEYCODE_L, + KeyEvent.KEYCODE_Z, + KeyEvent.KEYCODE_X, + KeyEvent.KEYCODE_C, + KeyEvent.KEYCODE_V, + KeyEvent.KEYCODE_B, + KeyEvent.KEYCODE_N, + KeyEvent.KEYCODE_M, + KeyEvent.KEYCODE_ENTER, + KeyEvent.KEYCODE_SHIFT_LEFT, + KeyEvent.KEYCODE_ESCAPE, + ).toIntArray() + + private val CUSTOMIZABLE_KEYS = listOf( + KeyEvent.KEYCODE_Q, + KeyEvent.KEYCODE_W, + KeyEvent.KEYCODE_E, + KeyEvent.KEYCODE_R, + KeyEvent.KEYCODE_T, + KeyEvent.KEYCODE_Y, + KeyEvent.KEYCODE_U, + KeyEvent.KEYCODE_I, + KeyEvent.KEYCODE_O, + KeyEvent.KEYCODE_P, + KeyEvent.KEYCODE_A, + KeyEvent.KEYCODE_S, + KeyEvent.KEYCODE_D, + KeyEvent.KEYCODE_F, + KeyEvent.KEYCODE_G, + KeyEvent.KEYCODE_H, + KeyEvent.KEYCODE_J, + KeyEvent.KEYCODE_K, + KeyEvent.KEYCODE_L, + KeyEvent.KEYCODE_Z, + KeyEvent.KEYCODE_X, + KeyEvent.KEYCODE_C, + KeyEvent.KEYCODE_V, + KeyEvent.KEYCODE_B, + KeyEvent.KEYCODE_N, + KeyEvent.KEYCODE_M, + KeyEvent.KEYCODE_ENTER, + KeyEvent.KEYCODE_SHIFT_LEFT, + ) + + private val DEFAULT_BINDINGS = mapOf( + KeyEvent.KEYCODE_DPAD_UP to KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_DPAD_DOWN to KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_DPAD_LEFT to KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_RIGHT to KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.KEYCODE_W to KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_A to KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_S to KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_D to KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.KEYCODE_I to KeyEvent.KEYCODE_BUTTON_X, + KeyEvent.KEYCODE_J to KeyEvent.KEYCODE_BUTTON_Y, + KeyEvent.KEYCODE_K to KeyEvent.KEYCODE_BUTTON_B, + KeyEvent.KEYCODE_L to KeyEvent.KEYCODE_BUTTON_A, + KeyEvent.KEYCODE_Q to KeyEvent.KEYCODE_BUTTON_L1, + KeyEvent.KEYCODE_E to KeyEvent.KEYCODE_BUTTON_L2, + KeyEvent.KEYCODE_U to KeyEvent.KEYCODE_BUTTON_R1, + KeyEvent.KEYCODE_O to KeyEvent.KEYCODE_BUTTON_R2, + KeyEvent.KEYCODE_ENTER to KeyEvent.KEYCODE_BUTTON_START, + KeyEvent.KEYCODE_SHIFT_LEFT to KeyEvent.KEYCODE_BUTTON_SELECT, + KeyEvent.KEYCODE_ESCAPE to KeyEvent.KEYCODE_BUTTON_MODE, + ).withDefault { KeyEvent.KEYCODE_UNKNOWN } + + override fun getInputKeys() = INPUT_KEYS + + override fun getAxesMap() = emptyMap() + + override fun getDefaultBindings() = DEFAULT_BINDINGS + + override fun getCustomizableKeys(): List = CUSTOMIZABLE_KEYS + + override fun isEnabledByDefault(appContext: Context): Boolean { + return !appContext.packageManager.hasSystemFeature("android.hardware.touchscreen") + } + + override fun getSupportedShortcuts(): List = emptyList() + + override fun isSupported(device: InputDevice): Boolean { + return sequenceOf( + (device.sources and InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD, + device.hasKeys(*MINIMAL_SUPPORTED_KEYS).all { it }, + device.isVirtual.not() + ).all { it } + } +} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/input/InputClassUnknown.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/input/InputClassUnknown.kt new file mode 100644 index 0000000000..1fa9b78ee6 --- /dev/null +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/input/InputClassUnknown.kt @@ -0,0 +1,21 @@ +package com.swordfish.lemuroid.app.shared.input + +import android.content.Context +import android.view.InputDevice +import com.swordfish.lemuroid.app.shared.settings.GameMenuShortcut + +object InputClassUnknown : InputClass { + override fun getInputKeys(): List = emptyList() + + override fun getAxesMap(): Map = emptyMap() + + override fun getDefaultBindings(): Map = emptyMap() + + override fun getCustomizableKeys(): List = emptyList() + + override fun isSupported(device: InputDevice): Boolean = false + + override fun isEnabledByDefault(appContext: Context): Boolean = false + + override fun getSupportedShortcuts(): List = emptyList() +} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/GamePadManager.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/input/InputDeviceManager.kt similarity index 64% rename from lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/GamePadManager.kt rename to lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/input/InputDeviceManager.kt index d231f66fd9..05af7e3f9a 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/GamePadManager.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/input/InputDeviceManager.kt @@ -1,14 +1,14 @@ -package com.swordfish.lemuroid.app.shared.settings +package com.swordfish.lemuroid.app.shared.input import android.content.Context import android.content.SharedPreferences import android.hardware.input.InputManager import android.view.InputDevice import android.view.KeyEvent -import android.view.MotionEvent import com.f2prateek.rx.preferences2.RxSharedPreferences import com.gojuno.koptional.Optional import com.gojuno.koptional.toOptional +import com.swordfish.lemuroid.app.shared.settings.GameMenuShortcut import io.reactivex.Completable import io.reactivex.Observable import io.reactivex.Single @@ -17,8 +17,8 @@ import io.reactivex.schedulers.Schedulers import io.reactivex.subjects.BehaviorSubject import dagger.Lazy -class GamePadManager( - context: Context, +class InputDeviceManager( + private val context: Context, sharedPreferencesFactory: Lazy ) { @@ -30,8 +30,8 @@ class GamePadManager( RxSharedPreferences.create(sharedPreferencesFactory.get()) } - fun getGamePadsBindingsObservable(): Observable<(InputDevice?)->Map> { - return getEnabledGamePadsObservable() + fun getInputBindingsObservable(): Observable<(InputDevice?)->Map> { + return getEnabledInputsObservable() .observeOn(Schedulers.io()) .flatMapSingle { inputDevices -> Observable.fromIterable(inputDevices).flatMapSingle { inputDevice -> @@ -42,24 +42,25 @@ class GamePadManager( .map { bindings -> { bindings[it] ?: mapOf() } } } - fun getGamePadMenuShortCutObservable(): Observable> { - return getEnabledGamePadsObservable() + fun getInputMenuShortCutObservable(): Observable> { + return getEnabledInputsObservable() .observeOn(Schedulers.io()) .map { devices -> - devices.firstOrNull() + val device = devices.firstOrNull() + device ?.let { sharedPreferences.getString( computeGameMenuShortcutPreference(it), GameMenuShortcut.getDefault(it)?.name ) } - ?.let { GameMenuShortcut.findByName(it) } + ?.let { GameMenuShortcut.findByName(device, it) } .toOptional() } } fun getGamePadsPortMapperObservable(): Observable<(InputDevice?)->Int?> { - return getEnabledGamePadsObservable().map { gamePads -> + return getEnabledInputsObservable().map { gamePads -> val portMappings = gamePads .mapIndexed { index, inputDevice -> inputDevice.id to index } .toMap() @@ -68,7 +69,7 @@ class GamePadManager( } private fun getBindings(inputDevice: InputDevice): Single> { - return Observable.fromIterable(INPUT_KEYS) + return Observable.fromIterable(inputDevice.getInputClass().getInputKeys()) .flatMapSingle { keyCode -> retrieveMappingFromPreferences( inputDevice, @@ -113,7 +114,7 @@ class GamePadManager( .subscribeOn(AndroidSchedulers.mainThread()) } - fun getEnabledGamePadsObservable(): Observable> { + fun getEnabledInputsObservable(): Observable> { return getGamePadsObservable() .flatMap { devices -> if (devices.isEmpty()) { @@ -123,7 +124,8 @@ class GamePadManager( val enabledGamePads = devices.map { device -> rxSharedPreferences .flatMapObservable { - it.getBoolean(computeEnabledGamePadPreference(device), true).asObservable() + val defaultValue = device.getInputClass().isEnabledByDefault(context) + it.getBoolean(computeEnabledGamePadPreference(device), defaultValue).asObservable() } } @@ -139,34 +141,30 @@ class GamePadManager( ): Single { val valueSingle = Single.fromCallable { val sharedPreferencesKey = computeKeyBindingPreference(inputDevice, keyCode) - val sharedPreferencesDefault = getDefaultBinding(keyCode).toString() + val sharedPreferencesDefault = getDefaultBinding(inputDevice, keyCode).toString() sharedPreferences.getString(sharedPreferencesKey, sharedPreferencesDefault) } return valueSingle.map { it.toInt() }.subscribeOn(Schedulers.io()) } - fun getDefaultBinding(keyCode: Int) = DEFAULT_BINDINGS.getValue(keyCode) + fun getDefaultBinding(inputDevice: InputDevice, keyCode: Int): Int { + return inputDevice + .getInputClass() + .getDefaultBindings() + .getValue(keyCode) + } private fun getAllGamePads(): List { return runCatching { InputDevice.getDeviceIds() .map { InputDevice.getDevice(it) } - .filter { isGamePad(it) } + .filter { it.getInputClass().isSupported(it) } + .filter { it.name !in BLACKLISTED_DEVICES } .sortedBy { it.controllerNumber } }.getOrNull() ?: listOf() } - private fun isGamePad(device: InputDevice): Boolean { - return sequenceOf( - device.name !in BLACKLISTED_DEVICES, - device.sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD, - device.hasKeys(*MINIMAL_SUPPORTED_KEYS).all { it }, - device.isVirtual.not(), - device.controllerNumber > 0 - ).all { it } - } - companion object { private const val GAME_PAD_BINDING_PREFERENCE_BASE_KEY = "pref_key_gamepad_binding" private const val GAME_PAD_ENABLED_PREFERENCE_BASE_KEY = "pref_key_gamepad_enabled" @@ -179,13 +177,6 @@ class GamePadManager( "virtual-search" ) - private val MINIMAL_SUPPORTED_KEYS = intArrayOf( - KeyEvent.KEYCODE_BUTTON_A, - KeyEvent.KEYCODE_BUTTON_B, - KeyEvent.KEYCODE_BUTTON_X, - KeyEvent.KEYCODE_BUTTON_Y, - ) - fun computeEnabledGamePadPreference(inputDevice: InputDevice) = "${GAME_PAD_ENABLED_PREFERENCE_BASE_KEY}_${getSharedPreferencesId(inputDevice)}" @@ -195,47 +186,6 @@ class GamePadManager( fun computeKeyBindingPreference(inputDevice: InputDevice, keyCode: Int) = "${GAME_PAD_BINDING_PREFERENCE_BASE_KEY}_${getSharedPreferencesId(inputDevice)}_$keyCode" - val TRIGGER_MOTIONS_TO_KEYS = mapOf( - MotionEvent.AXIS_BRAKE to KeyEvent.KEYCODE_BUTTON_L2, - MotionEvent.AXIS_THROTTLE to KeyEvent.KEYCODE_BUTTON_R2, - MotionEvent.AXIS_LTRIGGER to KeyEvent.KEYCODE_BUTTON_L2, - MotionEvent.AXIS_RTRIGGER to KeyEvent.KEYCODE_BUTTON_R2 - ) - - val INPUT_KEYS = listOf( - KeyEvent.KEYCODE_BUTTON_A, - KeyEvent.KEYCODE_BUTTON_B, - KeyEvent.KEYCODE_BUTTON_X, - KeyEvent.KEYCODE_BUTTON_Y, - KeyEvent.KEYCODE_BUTTON_START, - KeyEvent.KEYCODE_BUTTON_SELECT, - KeyEvent.KEYCODE_BUTTON_L1, - KeyEvent.KEYCODE_BUTTON_R1, - KeyEvent.KEYCODE_BUTTON_L2, - KeyEvent.KEYCODE_BUTTON_R2, - KeyEvent.KEYCODE_BUTTON_THUMBL, - KeyEvent.KEYCODE_BUTTON_THUMBR, - KeyEvent.KEYCODE_BUTTON_C, - KeyEvent.KEYCODE_BUTTON_Z, - KeyEvent.KEYCODE_BUTTON_1, - KeyEvent.KEYCODE_BUTTON_2, - KeyEvent.KEYCODE_BUTTON_3, - KeyEvent.KEYCODE_BUTTON_4, - KeyEvent.KEYCODE_BUTTON_5, - KeyEvent.KEYCODE_BUTTON_6, - KeyEvent.KEYCODE_BUTTON_7, - KeyEvent.KEYCODE_BUTTON_8, - KeyEvent.KEYCODE_BUTTON_9, - KeyEvent.KEYCODE_BUTTON_10, - KeyEvent.KEYCODE_BUTTON_11, - KeyEvent.KEYCODE_BUTTON_12, - KeyEvent.KEYCODE_BUTTON_13, - KeyEvent.KEYCODE_BUTTON_14, - KeyEvent.KEYCODE_BUTTON_15, - KeyEvent.KEYCODE_BUTTON_16, - KeyEvent.KEYCODE_BUTTON_MODE - ) - val OUTPUT_KEYS = listOf( KeyEvent.KEYCODE_BUTTON_A, KeyEvent.KEYCODE_BUTTON_B, @@ -250,14 +200,11 @@ class GamePadManager( KeyEvent.KEYCODE_BUTTON_THUMBL, KeyEvent.KEYCODE_BUTTON_THUMBR, KeyEvent.KEYCODE_BUTTON_MODE, - KeyEvent.KEYCODE_UNKNOWN + KeyEvent.KEYCODE_UNKNOWN, + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_DPAD_RIGHT, ) - - private val DEFAULT_BINDINGS = mapOf( - KeyEvent.KEYCODE_BUTTON_A to KeyEvent.KEYCODE_BUTTON_B, - KeyEvent.KEYCODE_BUTTON_B to KeyEvent.KEYCODE_BUTTON_A, - KeyEvent.KEYCODE_BUTTON_X to KeyEvent.KEYCODE_BUTTON_Y, - KeyEvent.KEYCODE_BUTTON_Y to KeyEvent.KEYCODE_BUTTON_X - ).withDefault { if (it in OUTPUT_KEYS) it else KeyEvent.KEYCODE_UNKNOWN } } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/LibraryIndexBroadcastReceiver.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/LibraryIndexBroadcastReceiver.kt new file mode 100644 index 0000000000..9677e6ed07 --- /dev/null +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/LibraryIndexBroadcastReceiver.kt @@ -0,0 +1,12 @@ +package com.swordfish.lemuroid.app.shared.library + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +class LibraryIndexBroadcastReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context?, intent: Intent?) { + LibraryIndexScheduler.cancelLibrarySync(context!!.applicationContext) + } +} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/LibraryIndexMonitor.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/LibraryIndexMonitor.kt deleted file mode 100644 index 6856c6aa59..0000000000 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/LibraryIndexMonitor.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.swordfish.lemuroid.app.shared.library - -import android.content.Context -import androidx.lifecycle.LiveData -import androidx.lifecycle.Transformations -import androidx.work.WorkInfo -import androidx.work.WorkManager -import com.swordfish.lemuroid.app.utils.livedata.ThrottledLiveData - -class LibraryIndexMonitor(private val appContext: Context) { - - fun getLiveData(): LiveData { - val workInfosLiveData = WorkManager.getInstance(appContext) - .getWorkInfosForUniqueWorkLiveData(LibraryIndexScheduler.UNIQUE_WORK_ID) - - val result = Transformations.map(workInfosLiveData) { workInfos -> - val isRunning = workInfos - .map { it.state } - .any { it in listOf(WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED) } - - isRunning - } - - return ThrottledLiveData(result, 200) - } -} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/LibraryIndexScheduler.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/LibraryIndexScheduler.kt index 5a95c9a3e4..1cd28049f2 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/LibraryIndexScheduler.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/LibraryIndexScheduler.kt @@ -6,26 +6,30 @@ import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager object LibraryIndexScheduler { - val UNIQUE_WORK_ID: String = LibraryIndexScheduler::class.java.simpleName + val CORE_UPDATE_WORK_ID: String = CoreUpdateWork::class.java.simpleName + val LIBRARY_INDEX_WORK_ID: String = LibraryIndexWork::class.java.simpleName - fun scheduleFullSync(applicationContext: Context) { + fun scheduleLibrarySync(applicationContext: Context) { WorkManager.getInstance(applicationContext) .beginUniqueWork( - UNIQUE_WORK_ID, - ExistingWorkPolicy.APPEND, + LIBRARY_INDEX_WORK_ID, + ExistingWorkPolicy.APPEND_OR_REPLACE, OneTimeWorkRequestBuilder().build() ) - .then(OneTimeWorkRequestBuilder().build()) .enqueue() } fun scheduleCoreUpdate(applicationContext: Context) { WorkManager.getInstance(applicationContext) .beginUniqueWork( - UNIQUE_WORK_ID, - ExistingWorkPolicy.APPEND, + CORE_UPDATE_WORK_ID, + ExistingWorkPolicy.APPEND_OR_REPLACE, OneTimeWorkRequestBuilder().build() ) .enqueue() } + + fun cancelLibrarySync(applicationContext: Context) { + WorkManager.getInstance(applicationContext).cancelUniqueWork(LIBRARY_INDEX_WORK_ID) + } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/LibraryIndexWork.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/LibraryIndexWork.kt index 99b4ec4767..05ddf13af9 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/LibraryIndexWork.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/LibraryIndexWork.kt @@ -35,6 +35,7 @@ class LibraryIndexWork(context: Context, workerParams: WorkerParameters) : return lemuroidLibrary.indexLibrary() .toSingleDefault(Result.success()) .doOnError { Timber.e(it, "Library indexing failed with exception: $it") } + .doFinally { LibraryIndexScheduler.scheduleCoreUpdate(applicationContext) } .onErrorReturn { Result.success() } // We need to return success or the Work chain will die forever. } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/PendingOperationsMonitor.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/PendingOperationsMonitor.kt new file mode 100644 index 0000000000..0b723b2a82 --- /dev/null +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/PendingOperationsMonitor.kt @@ -0,0 +1,61 @@ +package com.swordfish.lemuroid.app.shared.library + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.work.WorkInfo +import androidx.work.WorkManager +import com.swordfish.lemuroid.app.shared.savesync.SaveSyncWork +import com.swordfish.lemuroid.app.utils.livedata.map +import com.swordfish.lemuroid.app.utils.livedata.throttle +import com.swordfish.lemuroid.app.utils.livedata.combineLatest + +class PendingOperationsMonitor(private val appContext: Context) { + + enum class Operation(val uniqueId: String, val isPeriodic: Boolean) { + LIBRARY_INDEX(LibraryIndexScheduler.LIBRARY_INDEX_WORK_ID, false), + CORE_UPDATE(LibraryIndexScheduler.CORE_UPDATE_WORK_ID, false), + SAVES_SYNC_PERIODIC(SaveSyncWork.UNIQUE_PERIODIC_WORK_ID, true), + SAVES_SYNC_ONE_SHOT(SaveSyncWork.UNIQUE_WORK_ID, false) + } + + fun anyOperationInProgress(): LiveData { + return operationsInProgress(*Operation.values()) + } + + fun anySaveOperationInProgress(): LiveData { + return operationsInProgress(Operation.SAVES_SYNC_ONE_SHOT, Operation.SAVES_SYNC_PERIODIC) + } + + fun anyLibraryOperationInProgress(): LiveData { + return operationsInProgress(Operation.LIBRARY_INDEX, Operation.CORE_UPDATE) + } + + fun isDirectoryScanInProgress(): LiveData { + return operationsInProgress(Operation.LIBRARY_INDEX) + } + + private fun operationsInProgress(vararg operations: Operation): LiveData { + return operations + .map { operationInProgress(it) } + .reduce { first, second -> first.combineLatest(second) { b1, b2 -> b1 || b2 } } + .throttle(100) + } + + private fun operationInProgress(operation: Operation): LiveData { + return WorkManager.getInstance(appContext) + .getWorkInfosForUniqueWorkLiveData(operation.uniqueId) + .map { if (operation.isPeriodic) isPeriodicJobRunning(it) else isJobRunning(it) } + } + + private fun isJobRunning(workInfos: List): Boolean { + return workInfos + .map { it.state } + .any { it in listOf(WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED) } + } + + private fun isPeriodicJobRunning(workInfos: List): Boolean { + return workInfos + .map { it.state } + .any { it in listOf(WorkInfo.State.RUNNING) } + } +} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/main/PostGameHandler.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/main/GameLaunchTaskHandler.kt similarity index 67% rename from lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/main/PostGameHandler.kt rename to lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/main/GameLaunchTaskHandler.kt index 3890196113..eb8aa8c286 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/main/PostGameHandler.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/main/GameLaunchTaskHandler.kt @@ -1,9 +1,12 @@ package com.swordfish.lemuroid.app.shared.main import android.app.Activity +import android.content.Context import android.content.Intent import com.swordfish.lemuroid.app.shared.game.BaseGameActivity import com.swordfish.lemuroid.app.shared.gamecrash.GameCrashActivity +import com.swordfish.lemuroid.app.shared.savesync.SaveSyncWork +import com.swordfish.lemuroid.app.shared.storage.cache.CacheCleanerWork import com.swordfish.lemuroid.ext.feature.review.ReviewManager import com.swordfish.lemuroid.lib.library.db.RetrogradeDatabase import com.swordfish.lemuroid.lib.library.db.dao.updateAsync @@ -14,20 +17,37 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import java.util.concurrent.TimeUnit -class PostGameHandler( +class GameLaunchTaskHandler( private val reviewManager: ReviewManager, private val retrogradeDb: RetrogradeDatabase ) { - fun handle(enableRatingFlow: Boolean, activity: Activity, resultCode: Int, data: Intent?): Completable { + fun handleGameStart(context: Context) { + cancelBackgroundWork(context) + } + + fun handleGameFinish(enableRatingFlow: Boolean, activity: Activity, resultCode: Int, data: Intent?): Completable { + rescheduleBackgroundWork(activity.applicationContext) return if (resultCode == Activity.RESULT_OK) { - handleSuccessfulGame(activity, enableRatingFlow, data) + handleSuccessfulGameFinish(activity, enableRatingFlow, data) } else { - handleUnsuccessfulGame(activity, data) + handleUnsuccessfulGameFinish(activity, data) } } - private fun handleUnsuccessfulGame(activity: Activity, data: Intent?): Completable { + private fun cancelBackgroundWork(context: Context) { + SaveSyncWork.cancelAutoWork(context) + SaveSyncWork.cancelManualWork(context) + CacheCleanerWork.cancelCleanCacheLRU(context) + } + + private fun rescheduleBackgroundWork(context: Context) { + // Let's slightly delay the sync. Maybe the user wants to play another game. + SaveSyncWork.enqueueAutoWork(context, 5) + CacheCleanerWork.enqueueCleanCacheLRU(context) + } + + private fun handleUnsuccessfulGameFinish(activity: Activity, data: Intent?): Completable { return Completable.fromAction { val message = data?.getStringExtra(BaseGameActivity.PLAY_GAME_RESULT_ERROR) val intent = Intent(activity, GameCrashActivity::class.java).apply { @@ -37,7 +57,7 @@ class PostGameHandler( }.subscribeOn(AndroidSchedulers.mainThread()) } - private fun handleSuccessfulGame( + private fun handleSuccessfulGameFinish( activity: Activity, enableRatingFlow: Boolean, data: Intent? diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/rumble/RumbleManager.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/rumble/RumbleManager.kt new file mode 100644 index 0000000000..ace39f35ba --- /dev/null +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/rumble/RumbleManager.kt @@ -0,0 +1,91 @@ +package com.swordfish.lemuroid.app.shared.rumble + +import android.content.Context +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import android.view.InputDevice +import com.swordfish.lemuroid.app.mobile.feature.settings.RxSettingsManager +import com.swordfish.lemuroid.app.shared.input.InputDeviceManager +import com.swordfish.lemuroid.lib.library.SystemCoreConfig +import com.swordfish.libretrodroid.RumbleEvent +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import java.util.concurrent.Executors +import kotlin.math.roundToInt + +class RumbleManager( + applicationContext: Context, + private val rxSettingsManager: RxSettingsManager, + private val inputDeviceManager: InputDeviceManager +) { + private val deviceVibrator = applicationContext.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + private val singleThreadExecutor = Executors.newSingleThreadExecutor() + + fun processRumbleEvents( + systemCoreConfig: SystemCoreConfig, + rumbleEventsObservable: Observable + ): Completable { + return rxSettingsManager.enableRumble + .filter { it && systemCoreConfig.rumbleSupported } + .flatMapObservable { inputDeviceManager.getEnabledInputsObservable() } + .flatMapSingle { getVibrators(it) } + .switchMapCompletable { vibrators -> + rumbleEventsObservable + .subscribeOn(Schedulers.from(singleThreadExecutor)) + .doOnNext { + kotlin.runCatching { vibrate(vibrators[it.port], it) } + } + .doOnSubscribe { stopAllVibrators(vibrators) } + .doAfterTerminate { stopAllVibrators(vibrators) } + .ignoreElements() + .onErrorComplete() + } + } + + private fun stopAllVibrators(vibrators: List) { + vibrators.forEach { + kotlin.runCatching { it.cancel() } + } + } + + private fun getVibrators(gamePads: List): Single> { + return rxSettingsManager.enableDeviceRumble + .map { enableDeviceRumble -> + if (gamePads.isEmpty() && enableDeviceRumble) { + listOf(deviceVibrator) + } else { + gamePads.map { it.vibrator } + } + } + } + + private fun vibrate(vibrator: Vibrator?, rumbleEvent: RumbleEvent) { + if (vibrator == null) return + + vibrator.cancel() + + val amplitude = computeAmplitude(rumbleEvent) + + if (amplitude == 0) return + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && vibrator.hasAmplitudeControl()) { + vibrator.vibrate(VibrationEffect.createOneShot(MAX_RUMBLE_DURATION_MS, amplitude)) + } else if (amplitude > LEGACY_MIN_RUMBLE_STRENGTH) { + vibrator.vibrate(MAX_RUMBLE_DURATION_MS) + } + } + + private fun computeAmplitude(rumbleEvent: RumbleEvent): Int { + val strength = rumbleEvent.strengthStrong * 0.66f + rumbleEvent.strengthWeak * 0.33f + return (DEFAULT_RUMBLE_STRENGTH * (strength) * 255).roundToInt() + } + + companion object { + const val MAX_RUMBLE_DURATION_MS = 1000L + const val DEFAULT_RUMBLE_STRENGTH = 0.5f + const val LEGACY_MIN_RUMBLE_STRENGTH = 100 + } +} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/savesync/SaveSyncMonitor.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/savesync/SaveSyncMonitor.kt deleted file mode 100644 index 5b4acf9c45..0000000000 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/savesync/SaveSyncMonitor.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.swordfish.lemuroid.app.shared.savesync - -import android.content.Context -import androidx.lifecycle.LiveData -import androidx.lifecycle.Transformations -import androidx.work.WorkInfo -import androidx.work.WorkManager -import com.swordfish.lemuroid.app.utils.livedata.CombinedLiveData - -class SaveSyncMonitor(private val appContext: Context) { - - fun getLiveData(): LiveData { - return CombinedLiveData(getPeriodicLiveData(), getOneTimeLiveData()) { b1, b2 -> b1 || b2 } - } - - private fun getPeriodicLiveData(): LiveData { - val workInfosLiveData = WorkManager.getInstance(appContext) - .getWorkInfosForUniqueWorkLiveData(SaveSyncWork.UNIQUE_PERIODIC_WORK_ID) - - return Transformations.map(workInfosLiveData) { workInfos -> - val isRunning = workInfos - .map { it.state } - .any { it in listOf(WorkInfo.State.RUNNING) } - - isRunning - } - } - - private fun getOneTimeLiveData(): LiveData { - val workInfosLiveData = WorkManager.getInstance(appContext) - .getWorkInfosForUniqueWorkLiveData(SaveSyncWork.UNIQUE_WORK_ID) - - return Transformations.map(workInfosLiveData) { workInfos -> - val isRunning = workInfos - .map { it.state } - .any { it in listOf(WorkInfo.State.ENQUEUED, WorkInfo.State.RUNNING) } - - isRunning - } - } -} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/savesync/SaveSyncWork.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/savesync/SaveSyncWork.kt index 3b39db5adb..064d7a572b 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/savesync/SaveSyncWork.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/savesync/SaveSyncWork.kt @@ -23,6 +23,7 @@ import com.swordfish.lemuroid.lib.savesync.SaveSyncManager import dagger.Binds import dagger.android.AndroidInjector import dagger.multibindings.IntoMap +import io.reactivex.Scheduler import io.reactivex.Single import io.reactivex.schedulers.Schedulers import timber.log.Timber @@ -59,6 +60,10 @@ class SaveSyncWork(context: Context, workerParams: WorkerParameters) : .andThen(Single.just(Result.success())) } + override fun getBackgroundScheduler(): Scheduler { + return Schedulers.io() + } + private fun shouldPerformSync(): Single { val isAutoSync = inputData.getBoolean(IS_AUTO, false) val isManualSync = !isAutoSync diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/AdvancedSettingsPreferences.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/AdvancedSettingsPreferences.kt new file mode 100644 index 0000000000..7a0e0af637 --- /dev/null +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/AdvancedSettingsPreferences.kt @@ -0,0 +1,36 @@ +package com.swordfish.lemuroid.app.shared.settings + +import android.content.Context +import android.text.format.Formatter +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import com.swordfish.lemuroid.R +import com.swordfish.lemuroid.lib.storage.cache.CacheCleaner + +object AdvancedSettingsPreferences { + + fun updateCachePreferences(preferenceScreen: PreferenceScreen) { + val cacheKey = preferenceScreen.context.getString(R.string.pref_key_max_cache_size) + preferenceScreen.findPreference(cacheKey)?.apply { + val supportedCacheValues = CacheCleaner.getSupportedCacheLimits() + + entries = supportedCacheValues + .map { getSizeLabel(preferenceScreen.context, it) } + .toTypedArray() + + entryValues = supportedCacheValues + .map { it.toString() } + .toTypedArray() + + if (value == null) { + setValueIndex(supportedCacheValues.indexOf(CacheCleaner.getDefaultCacheLimit())) + } + + summaryProvider = ListPreference.SimpleSummaryProvider.getInstance() + } + } + + private fun getSizeLabel(appContext: Context, size: Long): String { + return Formatter.formatShortFileSize(appContext, size) + } +} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/GameMenuShortcut.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/GameMenuShortcut.kt index 3344cdfefb..fb147c91f5 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/GameMenuShortcut.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/GameMenuShortcut.kt @@ -1,32 +1,24 @@ package com.swordfish.lemuroid.app.shared.settings import android.view.InputDevice -import android.view.KeyEvent +import com.swordfish.lemuroid.app.shared.input.getInputClass data class GameMenuShortcut(val name: String, val keys: Set) { companion object { fun getDefault(inputDevice: InputDevice): GameMenuShortcut? { - return ALL_SHORTCUTS + return inputDevice.getInputClass() + .getSupportedShortcuts() .firstOrNull { shortcut -> inputDevice.hasKeys(*(shortcut.keys.toIntArray())).all { it } } } - val ALL_SHORTCUTS = listOf( - GameMenuShortcut( - "L3 + R3", - setOf(KeyEvent.KEYCODE_BUTTON_THUMBL, KeyEvent.KEYCODE_BUTTON_THUMBR) - ), - GameMenuShortcut( - "Select + Start", - setOf(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_BUTTON_SELECT) - ) - ) - - fun findByName(name: String): GameMenuShortcut { - return ALL_SHORTCUTS.first { it.name == name } + fun findByName(device: InputDevice, name: String): GameMenuShortcut? { + return device.getInputClass() + .getSupportedShortcuts() + .firstOrNull { it.name == name } } } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/GamePadPreferencesHelper.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/GamePadPreferencesHelper.kt index 1ee12fb762..58db7ec475 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/GamePadPreferencesHelper.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/GamePadPreferencesHelper.kt @@ -9,11 +9,13 @@ import androidx.preference.PreferenceCategory import androidx.preference.PreferenceScreen import androidx.preference.SwitchPreference import com.swordfish.lemuroid.R +import com.swordfish.lemuroid.app.shared.input.InputDeviceManager +import com.swordfish.lemuroid.app.shared.input.getInputClass -class GamePadPreferencesHelper(private val gamePadManager: GamePadManager) { +class GamePadPreferencesHelper(private val inputDeviceManager: InputDeviceManager) { - fun resetBindingsAndRefresh() = gamePadManager.resetAllBindings() - .andThen(gamePadManager.getGamePadsObservable().firstElement()) + fun resetBindingsAndRefresh() = inputDeviceManager.resetAllBindings() + .andThen(inputDeviceManager.getGamePadsObservable().firstElement()) fun addGamePadsPreferencesToScreen( context: Context, @@ -79,7 +81,7 @@ class GamePadPreferencesHelper(private val gamePadManager: GamePadManager) { val category = createCategory(context, preferenceScreen, inputDevice.name) preferenceScreen.addPreference(category) - GamePadManager.INPUT_KEYS + inputDevice.getInputClass().getCustomizableKeys() .filter { inputDevice.hasKeys(it)[0] } .map { buildKeyBindingPreference(context, inputDevice, it) } .forEach { @@ -93,20 +95,20 @@ class GamePadPreferencesHelper(private val gamePadManager: GamePadManager) { private fun buildGamePadEnabledPreference(context: Context, inputDevice: InputDevice): Preference { val preference = SwitchPreference(context) - preference.key = GamePadManager.computeEnabledGamePadPreference(inputDevice) + preference.key = InputDeviceManager.computeEnabledGamePadPreference(inputDevice) preference.title = inputDevice.name - preference.setDefaultValue(true) + preference.setDefaultValue(inputDevice.getInputClass().isEnabledByDefault(context)) preference.isIconSpaceReserved = false return preference } private fun buildKeyBindingPreference(context: Context, inputDevice: InputDevice, key: Int): Preference { - val outputKeys = GamePadManager.OUTPUT_KEYS + val outputKeys = InputDeviceManager.OUTPUT_KEYS val outputKeysName = outputKeys.map { getRetroPadKeyName(context, it) } - val defaultBinding = gamePadManager.getDefaultBinding(key) + val defaultBinding = inputDeviceManager.getDefaultBinding(inputDevice, key) val preference = ListPreference(context) - preference.key = GamePadManager.computeKeyBindingPreference(inputDevice, key) + preference.key = InputDeviceManager.computeKeyBindingPreference(inputDevice, key) preference.title = getButtonKeyName(context, key) preference.entries = outputKeysName.toTypedArray() preference.entryValues = outputKeys.map { it.toString() }.toTypedArray() @@ -121,13 +123,14 @@ class GamePadPreferencesHelper(private val gamePadManager: GamePadManager) { private fun buildGameMenuShortcutPreference(context: Context, inputDevice: InputDevice): Preference? { val default = GameMenuShortcut.getDefault(inputDevice) ?: return null + val supportedShortcuts = inputDevice.getInputClass().getSupportedShortcuts() val preference = ListPreference(context) - preference.key = GamePadManager.computeGameMenuShortcutPreference(inputDevice) + preference.key = InputDeviceManager.computeGameMenuShortcutPreference(inputDevice) preference.title = context.getString(R.string.settings_gamepad_title_game_menu) - preference.entries = GameMenuShortcut.ALL_SHORTCUTS.map { it.name }.toTypedArray() - preference.entryValues = GameMenuShortcut.ALL_SHORTCUTS.map { it.name }.toTypedArray() - preference.setValueIndex(GameMenuShortcut.ALL_SHORTCUTS.indexOf(default)) + preference.entries = supportedShortcuts.map { it.name }.toTypedArray() + preference.entryValues = supportedShortcuts.map { it.name }.toTypedArray() + preference.setValueIndex(supportedShortcuts.indexOf(default)) preference.setDefaultValue(default.name) preference.summaryProvider = ListPreference.SimpleSummaryProvider.getInstance() preference.isIconSpaceReserved = false @@ -179,6 +182,38 @@ class GamePadPreferencesHelper(private val gamePadManager: GamePadManager) { KeyEvent.KEYCODE_BUTTON_MODE to "Option", KeyEvent.KEYCODE_BUTTON_Z to "Z", KeyEvent.KEYCODE_BUTTON_C to "C", + KeyEvent.KEYCODE_Q to "Q", + KeyEvent.KEYCODE_W to "W", + KeyEvent.KEYCODE_E to "E", + KeyEvent.KEYCODE_R to "R", + KeyEvent.KEYCODE_T to "T", + KeyEvent.KEYCODE_Y to "Y", + KeyEvent.KEYCODE_U to "U", + KeyEvent.KEYCODE_I to "I", + KeyEvent.KEYCODE_O to "O", + KeyEvent.KEYCODE_P to "P", + KeyEvent.KEYCODE_A to "A", + KeyEvent.KEYCODE_S to "S", + KeyEvent.KEYCODE_D to "D", + KeyEvent.KEYCODE_F to "F", + KeyEvent.KEYCODE_G to "G", + KeyEvent.KEYCODE_H to "H", + KeyEvent.KEYCODE_J to "J", + KeyEvent.KEYCODE_K to "K", + KeyEvent.KEYCODE_L to "L", + KeyEvent.KEYCODE_Z to "Z", + KeyEvent.KEYCODE_X to "X", + KeyEvent.KEYCODE_C to "C", + KeyEvent.KEYCODE_V to "V", + KeyEvent.KEYCODE_B to "B", + KeyEvent.KEYCODE_N to "N", + KeyEvent.KEYCODE_M to "M", + KeyEvent.KEYCODE_DPAD_UP to "Up", + KeyEvent.KEYCODE_DPAD_LEFT to "Left", + KeyEvent.KEYCODE_DPAD_RIGHT to "Right", + KeyEvent.KEYCODE_DPAD_DOWN to "Down", + KeyEvent.KEYCODE_ENTER to "Enter", + KeyEvent.KEYCODE_SHIFT_LEFT to "Shift", KeyEvent.KEYCODE_UNKNOWN to "" ) } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/SaveSyncPreferences.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/SaveSyncPreferences.kt index c4ff61cde4..ed892b61af 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/SaveSyncPreferences.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/SaveSyncPreferences.kt @@ -13,9 +13,7 @@ import com.swordfish.lemuroid.lib.library.CoreID import com.swordfish.lemuroid.lib.library.GameSystem import com.swordfish.lemuroid.lib.savesync.SaveSyncManager -class SaveSyncPreferences( - private val saveSyncManager: SaveSyncManager -) { +class SaveSyncPreferences(private val saveSyncManager: SaveSyncManager) { fun addSaveSyncPreferences(preferenceScreen: PreferenceScreen) { val context = preferenceScreen.context diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/SettingsInteractor.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/SettingsInteractor.kt index 2c188052d9..19bb7eacf8 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/SettingsInteractor.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/SettingsInteractor.kt @@ -4,7 +4,7 @@ import android.content.Context import com.swordfish.lemuroid.app.shared.library.LibraryIndexScheduler import com.swordfish.lemuroid.lib.preferences.SharedPreferencesHelper import com.swordfish.lemuroid.lib.storage.DirectoriesManager -import com.swordfish.lemuroid.lib.storage.cache.CacheCleanerWork +import com.swordfish.lemuroid.app.shared.storage.cache.CacheCleanerWork class SettingsInteractor( private val context: Context, @@ -17,7 +17,7 @@ class SettingsInteractor( fun resetAllSettings() { SharedPreferencesHelper.getLegacySharedPreferences(context).edit().clear().apply() SharedPreferencesHelper.getSharedPreferences(context).edit().clear().apply() - LibraryIndexScheduler.scheduleFullSync(context.applicationContext) + LibraryIndexScheduler.scheduleLibrarySync(context.applicationContext) CacheCleanerWork.enqueueCleanCacheAll(context.applicationContext) deleteDownloadedCores() } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/StorageFrameworkPickerLauncher.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/StorageFrameworkPickerLauncher.kt index c0f9ad11e0..70ab7a7413 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/StorageFrameworkPickerLauncher.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/settings/StorageFrameworkPickerLauncher.kt @@ -80,7 +80,7 @@ class StorageFrameworkPickerLauncher : RetrogradeActivity() { } private fun startLibraryIndexWork() { - LibraryIndexScheduler.scheduleFullSync(applicationContext) + LibraryIndexScheduler.scheduleLibrarySync(applicationContext) } companion object { diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/startup/DebugInitializer.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/startup/DebugInitializer.kt new file mode 100644 index 0000000000..bc6bd77c14 --- /dev/null +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/startup/DebugInitializer.kt @@ -0,0 +1,30 @@ +package com.swordfish.lemuroid.app.shared.startup + +import android.content.Context +import android.os.StrictMode +import androidx.startup.Initializer +import com.swordfish.lemuroid.BuildConfig +import timber.log.Timber + +class DebugInitializer : Initializer { + + override fun create(context: Context) { + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + enableStrictMode() + } + } + + private fun enableStrictMode() { + StrictMode.setThreadPolicy( + StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyLog() + .build() + ) + } + + override fun dependencies(): List>> { + return emptyList() + } +} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/startup/WorkManagerTasksInitializer.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/startup/WorkManagerTasksInitializer.kt new file mode 100644 index 0000000000..3bab2d4b73 --- /dev/null +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/startup/WorkManagerTasksInitializer.kt @@ -0,0 +1,43 @@ +package com.swordfish.lemuroid.app.shared.startup + +import android.app.ActivityManager +import android.content.Context +import android.os.Build +import android.os.Process +import androidx.startup.Initializer +import androidx.work.WorkManagerInitializer +import com.swordfish.lemuroid.app.shared.library.LibraryIndexScheduler +import com.swordfish.lemuroid.app.shared.savesync.SaveSyncWork +import dagger.android.support.DaggerApplication +import timber.log.Timber + +class WorkManagerTasksInitializer : Initializer { + override fun create(context: Context) { + Timber.i("Requested initialization of WorkManager tasks") + if (isMainProcess(context)) { + Timber.i("Running initial WorkManager tasks") + SaveSyncWork.enqueueAutoWork(context, 0) + LibraryIndexScheduler.scheduleCoreUpdate(context) + } + } + + override fun dependencies(): List>> { + return listOf(WorkManagerInitializer::class.java, DebugInitializer::class.java) + } + + private fun isMainProcess(context: Context): Boolean { + return retrieveProcessName(context) == context.packageName + } + + private fun retrieveProcessName(context: Context): String? { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + return DaggerApplication.getProcessName() + } + + val currentPID = Process.myPid() + val manager = context.getSystemService(DaggerApplication.ACTIVITY_SERVICE) as ActivityManager + return manager.runningAppProcesses + .firstOrNull { it.pid == currentPID } + ?.processName + } +} diff --git a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/storage/cache/CacheCleanerWork.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/storage/cache/CacheCleanerWork.kt similarity index 58% rename from retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/storage/cache/CacheCleanerWork.kt rename to lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/storage/cache/CacheCleanerWork.kt index 0887af4899..48ea60ba30 100644 --- a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/storage/cache/CacheCleanerWork.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/storage/cache/CacheCleanerWork.kt @@ -1,31 +1,48 @@ -package com.swordfish.lemuroid.lib.storage.cache +package com.swordfish.lemuroid.app.shared.storage.cache import android.content.Context import androidx.work.Data import androidx.work.ExistingWorkPolicy +import androidx.work.ListenableWorker import androidx.work.OneTimeWorkRequestBuilder import androidx.work.RxWorker import androidx.work.WorkManager import androidx.work.WorkerParameters import androidx.work.workDataOf +import com.swordfish.lemuroid.app.mobile.feature.settings.RxSettingsManager +import com.swordfish.lemuroid.lib.injection.AndroidWorkerInjection +import com.swordfish.lemuroid.lib.injection.WorkerKey +import com.swordfish.lemuroid.lib.storage.cache.CacheCleaner +import dagger.Binds +import dagger.android.AndroidInjector +import dagger.multibindings.IntoMap import io.reactivex.Completable import io.reactivex.Single import io.reactivex.schedulers.Schedulers +import javax.inject.Inject class CacheCleanerWork(context: Context, workerParams: WorkerParameters) : RxWorker(context, workerParams) { + @Inject lateinit var rxSettingsManager: RxSettingsManager + override fun createWork(): Single { + AndroidWorkerInjection.inject(this) + val cleanCompletable = if (inputData.getBoolean(CLEAN_EVERYTHING, false)) { createCleanAllCompletable(applicationContext) } else { createCleanLRUCompletable(applicationContext) } - return cleanCompletable.subscribeOn(Schedulers.io()).onErrorComplete().toSingleDefault(Result.success()) + return cleanCompletable + .subscribeOn(Schedulers.io()) + .onErrorComplete() + .toSingleDefault(Result.success()) } private fun createCleanLRUCompletable(context: Context): Completable { - val optimalCacheSize = CacheCleaner.getOptimalCacheSize() - return CacheCleaner.clean(context, optimalCacheSize) + return rxSettingsManager.cacheSizeBytes + .map { it.toLong() } + .flatMapCompletable { CacheCleaner.clean(context, it) } } private fun createCleanAllCompletable(context: Context): Completable { @@ -35,7 +52,7 @@ class CacheCleanerWork(context: Context, workerParams: WorkerParameters) : RxWor companion object { private val UNIQUE_WORK_ID: String = CacheCleanerWork::class.java.simpleName - private val CLEAN_EVERYTHING: String = "CLEAN_EVERYTHING" + private const val CLEAN_EVERYTHING: String = "CLEAN_EVERYTHING" fun enqueueCleanCacheLRU(applicationContext: Context) { WorkManager.getInstance(applicationContext).enqueueUniqueWork( @@ -61,4 +78,18 @@ class CacheCleanerWork(context: Context, workerParams: WorkerParameters) : RxWor ) } } + + @dagger.Module(subcomponents = [Subcomponent::class]) + abstract class Module { + @Binds + @IntoMap + @WorkerKey(CacheCleanerWork::class) + abstract fun bindMyWorkerFactory(builder: Subcomponent.Builder): AndroidInjector.Factory + } + + @dagger.Subcomponent + interface Subcomponent : AndroidInjector { + @dagger.Subcomponent.Builder + abstract class Builder : AndroidInjector.Builder() + } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/systems/MetaSystemInfo.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/systems/MetaSystemInfo.kt index 657e70c26b..471d7a3ffd 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/systems/MetaSystemInfo.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/systems/MetaSystemInfo.kt @@ -1,5 +1,8 @@ package com.swordfish.lemuroid.app.shared.systems +import android.content.Context import com.swordfish.lemuroid.lib.library.MetaSystemID -data class MetaSystemInfo(val metaSystem: MetaSystemID, val count: Int) +data class MetaSystemInfo(val metaSystem: MetaSystemID, val count: Int) { + fun getName(context: Context) = context.resources.getString(metaSystem.titleResId) +} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/channel/ChannelHandler.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/channel/ChannelHandler.kt index c1dcafb4ba..ea7a151001 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/channel/ChannelHandler.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/channel/ChannelHandler.kt @@ -1,5 +1,6 @@ package com.swordfish.lemuroid.app.tv.channel +import android.annotation.SuppressLint import android.content.ContentUris import android.content.Context import android.graphics.Bitmap @@ -34,7 +35,7 @@ class ChannelHandler( private val appName = appContext.getString(R.string.lemuroid_name) - private fun getOrCreateChannelId(): Long { + private fun getOrCreateChannelId(): Long? { var channelId = findChannel() if (channelId != null) @@ -48,7 +49,8 @@ class ChannelHandler( val channelUri = appContext.contentResolver.insert( TvContractCompat.Channels.CONTENT_URI, builder.build().toContentValues() - ) + ) ?: return null + channelId = ContentUris.parseId(channelUri) ChannelLogoUtils.storeChannelLogo( @@ -92,7 +94,7 @@ class ChannelHandler( } .toList() .doOnSuccess { - val channelId = getOrCreateChannelId() + val channelId = getOrCreateChannelId() ?: return@doOnSuccess val channel = Channel.Builder() channel.setDisplayName(appName) @@ -127,6 +129,7 @@ class ChannelHandler( .ignoreElement() } + @SuppressLint("RestrictedApi") private fun getGameProgram( channelId: Long, game: Game, diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/favorites/TVFavoritesFragment.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/favorites/TVFavoritesFragment.kt index 503496ee51..ecb8ddb5be 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/favorites/TVFavoritesFragment.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/favorites/TVFavoritesFragment.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.paging.cachedIn import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.shared.GameInteractor +import com.swordfish.lemuroid.app.shared.covers.CoverLoader import com.swordfish.lemuroid.app.tv.shared.GamePresenter import com.swordfish.lemuroid.lib.library.db.RetrogradeDatabase import com.swordfish.lemuroid.lib.library.db.entity.Game @@ -20,6 +21,7 @@ class TVFavoritesFragment : VerticalGridSupportFragment() { @Inject lateinit var retrogradeDb: RetrogradeDatabase @Inject lateinit var gameInteractor: GameInteractor + @Inject lateinit var coverLoader: CoverLoader override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -32,7 +34,10 @@ class TVFavoritesFragment : VerticalGridSupportFragment() { val gamesViewModel = ViewModelProvider(this, factory).get(TVFavoritesViewModel::class.java) val cardSize = resources.getDimensionPixelSize(R.dimen.card_size) - val pagingAdapter = PagingDataAdapter(GamePresenter(cardSize, gameInteractor), Game.DIFF_CALLBACK) + val pagingAdapter = PagingDataAdapter( + GamePresenter(cardSize, gameInteractor, coverLoader), + Game.DIFF_CALLBACK + ) this.adapter = pagingAdapter diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/folderpicker/TVFolderPickerLauncher.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/folderpicker/TVFolderPickerLauncher.kt index 3f1765cc31..9a29d7ef42 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/folderpicker/TVFolderPickerLauncher.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/folderpicker/TVFolderPickerLauncher.kt @@ -42,7 +42,7 @@ class TVFolderPickerLauncher : ImmersiveActivity() { } private fun startLibraryIndexWork() { - LibraryIndexScheduler.scheduleFullSync(applicationContext) + LibraryIndexScheduler.scheduleLibrarySync(applicationContext) } companion object { diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/game/TVGameActivity.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/game/TVGameActivity.kt index 7f5866a91b..4a36846f6c 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/game/TVGameActivity.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/game/TVGameActivity.kt @@ -17,8 +17,8 @@ class TVGameActivity : BaseGameActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - gamePadManager - .getEnabledGamePadsObservable() + inputDeviceManager + .getEnabledInputsObservable() .filter { it.isEmpty() } .autoDispose(scope()) .subscribeBy(Timber::e) { displayToast(R.string.tv_game_message_missing_gamepad) } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/gamemenu/TVGameMenuActivity.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/gamemenu/TVGameMenuActivity.kt index f5a690bc9d..74db692f85 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/gamemenu/TVGameMenuActivity.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/gamemenu/TVGameMenuActivity.kt @@ -4,7 +4,7 @@ import android.os.Bundle import androidx.fragment.app.Fragment import com.swordfish.lemuroid.app.shared.GameMenuContract import com.swordfish.lemuroid.app.shared.coreoptions.LemuroidCoreOption -import com.swordfish.lemuroid.app.shared.settings.GamePadManager +import com.swordfish.lemuroid.app.shared.input.InputDeviceManager import com.swordfish.lemuroid.app.tv.shared.TVBaseSettingsActivity import com.swordfish.lemuroid.lib.library.SystemCoreConfig import com.swordfish.lemuroid.lib.library.db.entity.Game @@ -17,7 +17,7 @@ class TVGameMenuActivity : TVBaseSettingsActivity() { @Inject lateinit var statesManager: StatesManager @Inject lateinit var statesPreviewManager: StatesPreviewManager - @Inject lateinit var gamePadManager: GamePadManager + @Inject lateinit var inputDeviceManager: InputDeviceManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -55,7 +55,7 @@ class TVGameMenuActivity : TVBaseSettingsActivity() { val fragment = TVGameMenuFragmentWrapper( statesManager, statesPreviewManager, - gamePadManager, + inputDeviceManager, game, core, options, @@ -79,7 +79,7 @@ class TVGameMenuActivity : TVBaseSettingsActivity() { class TVGameMenuFragmentWrapper( private val statesManager: StatesManager, private val statesPreviewManager: StatesPreviewManager, - private val gamePadManager: GamePadManager, + private val inputDeviceManager: InputDeviceManager, private val game: Game, private val systemCoreConfig: SystemCoreConfig, private val coreOptions: Array, @@ -95,7 +95,7 @@ class TVGameMenuActivity : TVBaseSettingsActivity() { return TVGameMenuFragment( statesManager, statesPreviewManager, - gamePadManager, + inputDeviceManager, game, systemCoreConfig, coreOptions, diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/gamemenu/TVGameMenuFragment.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/gamemenu/TVGameMenuFragment.kt index 4d67f09fa1..050551bf2c 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/gamemenu/TVGameMenuFragment.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/gamemenu/TVGameMenuFragment.kt @@ -8,7 +8,7 @@ import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.shared.coreoptions.CoreOptionsPreferenceHelper import com.swordfish.lemuroid.app.shared.coreoptions.LemuroidCoreOption import com.swordfish.lemuroid.app.shared.gamemenu.GameMenuHelper -import com.swordfish.lemuroid.app.shared.settings.GamePadManager +import com.swordfish.lemuroid.app.shared.input.InputDeviceManager import com.swordfish.lemuroid.common.rx.toSingleAsOptional import com.swordfish.lemuroid.lib.library.SystemCoreConfig import com.swordfish.lemuroid.lib.library.db.entity.Game @@ -25,7 +25,7 @@ import io.reactivex.schedulers.Schedulers class TVGameMenuFragment( private val statesManager: StatesManager, private val statesPreviewManager: StatesPreviewManager, - private val gamePadManager: GamePadManager, + private val inputDeviceManager: InputDeviceManager, private val game: Game, private val systemCoreConfig: SystemCoreConfig, private val coreOptions: Array, @@ -48,7 +48,7 @@ class TVGameMenuFragment( setupLoadAndSave() - gamePadManager.getGamePadsObservable() + inputDeviceManager.getGamePadsObservable() .observeOn(AndroidSchedulers.mainThread()) .autoDispose(scope()) .subscribeBy { diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/games/TVGamesFragment.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/games/TVGamesFragment.kt index 8b92624cf0..e8033dfe06 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/games/TVGamesFragment.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/games/TVGamesFragment.kt @@ -11,6 +11,7 @@ import androidx.navigation.fragment.navArgs import androidx.paging.cachedIn import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.shared.GameInteractor +import com.swordfish.lemuroid.app.shared.covers.CoverLoader import com.swordfish.lemuroid.app.tv.shared.GamePresenter import com.swordfish.lemuroid.lib.library.db.RetrogradeDatabase import com.swordfish.lemuroid.lib.library.db.entity.Game @@ -21,6 +22,7 @@ class TVGamesFragment : VerticalGridSupportFragment() { @Inject lateinit var retrogradeDb: RetrogradeDatabase @Inject lateinit var gameInteractor: GameInteractor + @Inject lateinit var coverLoader: CoverLoader private val args: TVGamesFragmentArgs by navArgs() @@ -35,7 +37,10 @@ class TVGamesFragment : VerticalGridSupportFragment() { val gamesViewModel = ViewModelProvider(this, factory).get(TVGamesViewModel::class.java) val cardSize = resources.getDimensionPixelSize(R.dimen.card_size) - val pagingAdapter = PagingDataAdapter(GamePresenter(cardSize, gameInteractor), Game.DIFF_CALLBACK) + val pagingAdapter = PagingDataAdapter( + GamePresenter(cardSize, gameInteractor, coverLoader), + Game.DIFF_CALLBACK + ) this.adapter = pagingAdapter diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/home/TVHomeFragment.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/home/TVHomeFragment.kt index c9150b85a7..b2f0aa9697 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/home/TVHomeFragment.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/home/TVHomeFragment.kt @@ -15,26 +15,28 @@ import androidx.leanback.widget.ListRow import androidx.leanback.widget.ListRowPresenter import androidx.leanback.widget.ObjectAdapter import androidx.leanback.widget.OnItemViewClickedListener -import androidx.lifecycle.LiveDataReactiveStreams import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.shared.GameInteractor +import com.swordfish.lemuroid.app.shared.covers.CoverLoader import com.swordfish.lemuroid.app.shared.library.LibraryIndexScheduler +import com.swordfish.lemuroid.app.shared.savesync.SaveSyncWork import com.swordfish.lemuroid.app.shared.settings.StorageFrameworkPickerLauncher import com.swordfish.lemuroid.app.shared.systems.MetaSystemInfo import com.swordfish.lemuroid.app.tv.folderpicker.TVFolderPickerLauncher import com.swordfish.lemuroid.app.tv.settings.TVSettingsActivity import com.swordfish.lemuroid.app.tv.shared.GamePresenter import com.swordfish.lemuroid.app.tv.shared.TVHelper +import com.swordfish.lemuroid.app.utils.livedata.toObservable import com.swordfish.lemuroid.common.rx.RXUtils import com.swordfish.lemuroid.lib.library.db.RetrogradeDatabase import com.swordfish.lemuroid.lib.library.db.entity.Game +import com.swordfish.lemuroid.lib.savesync.SaveSyncManager import com.swordfish.lemuroid.lib.util.subscribeBy import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider import com.uber.autodispose.autoDispose import dagger.android.support.AndroidSupportInjection -import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -43,6 +45,8 @@ class TVHomeFragment : BrowseSupportFragment() { @Inject lateinit var retrogradeDb: RetrogradeDatabase @Inject lateinit var gameInteractor: GameInteractor + @Inject lateinit var coverLoader: CoverLoader + @Inject lateinit var saveSyncManager: SaveSyncManager override fun onAttach(context: Context) { AndroidSupportInjection.inject(this) @@ -63,12 +67,18 @@ class TVHomeFragment : BrowseSupportFragment() { } is TVSetting -> { when (item.type) { - TVSettingType.RESCAN -> LibraryIndexScheduler.scheduleFullSync( + TVSettingType.STOP_RESCAN -> LibraryIndexScheduler.cancelLibrarySync( + requireContext().applicationContext + ) + TVSettingType.RESCAN -> LibraryIndexScheduler.scheduleLibrarySync( requireContext().applicationContext ) TVSettingType.CHOOSE_DIRECTORY -> launchFolderPicker() TVSettingType.SETTINGS -> launchTVSettings() TVSettingType.SHOW_ALL_FAVORITES -> launchFavorites() + TVSettingType.SAVE_SYNC -> SaveSyncWork.enqueueManualWork( + requireContext().applicationContext + ) } } } @@ -92,24 +102,23 @@ class TVHomeFragment : BrowseSupportFragment() { val factory = TVHomeViewModel.Factory(retrogradeDb, requireContext().applicationContext) val homeViewModel = ViewModelProvider(this, factory).get(TVHomeViewModel::class.java) - val indexingProgress: Observable = LiveDataReactiveStreams.toPublisher( - this, - homeViewModel.indexingInProgress - ).let { Observable.fromPublisher(it) } + val indexingProgress = homeViewModel.indexingInProgress.toObservable(this) + val directoryScanInProgress = homeViewModel.directoryScanInProgress.toObservable(this) val entriesObservable = RXUtils.combineLatest( homeViewModel.favoritesGames, homeViewModel.recentGames, homeViewModel.availableSystems, - indexingProgress + indexingProgress, + directoryScanInProgress ) entriesObservable .debounce(50, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .autoDispose(AndroidLifecycleScopeProvider.from(viewLifecycleOwner)) - .subscribeBy { (favoriteGames, recentGames, systems, inProgress) -> - update(favoriteGames, recentGames, systems, inProgress) + .subscribeBy { (favoriteGames, recentGames, systems, indexInProgress, scanInProgress) -> + update(favoriteGames, recentGames, systems, indexInProgress, scanInProgress) } } @@ -117,7 +126,8 @@ class TVHomeFragment : BrowseSupportFragment() { favoritesGames: List, recentGames: List, metaSystems: List, - scanningInProgress: Boolean + indexInProgress: Boolean, + scanInProgress: Boolean ) { val adapterHasFavorites = findAdapterById(FAVORITES_ADAPTER) != null val adapterHasGames = findAdapterById(RECENTS_ADAPTER) != null @@ -143,7 +153,7 @@ class TVHomeFragment : BrowseSupportFragment() { findAdapterById(RECENTS_ADAPTER)?.setItems(recentGames, LEANBACK_GAME_DIFF_CALLBACK) findAdapterById(SYSTEM_ADAPTER)?.setItems(metaSystems, LEANBACK_SYSTEM_DIFF_CALLBACK) findAdapterById(SETTINGS_ADAPTER)?.setItems( - buildSettingsRowItems(!scanningInProgress), + buildSettingsRowItems(indexInProgress, scanInProgress), LEANBACK_SETTING_DIFF_CALLBACK ) } @@ -169,7 +179,7 @@ class TVHomeFragment : BrowseSupportFragment() { if (includeFavorites) { val presenter = ClassPresenterSelector() - presenter.addClassPresenter(Game::class.java, GamePresenter(cardSize, gameInteractor)) + presenter.addClassPresenter(Game::class.java, GamePresenter(cardSize, gameInteractor, coverLoader)) presenter.addClassPresenter(TVSetting::class.java, SettingPresenter(cardSize, cardPadding)) val favouritesItems = ArrayObjectAdapter(presenter) val title = resources.getString(R.string.tv_home_section_favorites) @@ -177,7 +187,7 @@ class TVHomeFragment : BrowseSupportFragment() { } if (includeRecentGames) { - val recentItems = ArrayObjectAdapter(GamePresenter(cardSize, gameInteractor)) + val recentItems = ArrayObjectAdapter(GamePresenter(cardSize, gameInteractor, coverLoader)) val title = resources.getString(R.string.tv_home_section_recents) result.add(ListRow(HeaderItem(RECENTS_ADAPTER, title), recentItems)) } @@ -189,18 +199,32 @@ class TVHomeFragment : BrowseSupportFragment() { } val settingsItems = ArrayObjectAdapter(SettingPresenter(cardSize, cardPadding)) - settingsItems.setItems(buildSettingsRowItems(true), LEANBACK_SETTING_DIFF_CALLBACK) + settingsItems.setItems( + buildSettingsRowItems(indexInProgress = false, scanInProgress = false), + LEANBACK_SETTING_DIFF_CALLBACK + ) val settingsTitle = resources.getString(R.string.tv_home_section_settings) result.add(ListRow(HeaderItem(SETTINGS_ADAPTER, settingsTitle), settingsItems)) adapter = result } - private fun buildSettingsRowItems(rescanEnabled: Boolean): List { + private fun buildSettingsRowItems( + indexInProgress: Boolean, + scanInProgress: Boolean + ): List { return mutableListOf().apply { - add(TVSetting(TVSettingType.RESCAN, rescanEnabled)) - add(TVSetting(TVSettingType.CHOOSE_DIRECTORY, rescanEnabled)) - add(TVSetting(TVSettingType.SETTINGS)) + if (saveSyncManager.isSupported() && saveSyncManager.isConfigured()) { + add(TVSetting(TVSettingType.SAVE_SYNC, !indexInProgress)) + } + if (scanInProgress) { + add(TVSetting(TVSettingType.STOP_RESCAN, true)) + } else { + add(TVSetting(TVSettingType.RESCAN, !indexInProgress)) + } + + add(TVSetting(TVSettingType.CHOOSE_DIRECTORY, !indexInProgress)) + add(TVSetting(TVSettingType.SETTINGS, !indexInProgress)) } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/home/TVHomeViewModel.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/home/TVHomeViewModel.kt index 8e68a59098..9e5524c1b0 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/home/TVHomeViewModel.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/home/TVHomeViewModel.kt @@ -3,7 +3,7 @@ package com.swordfish.lemuroid.app.tv.home import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.swordfish.lemuroid.app.shared.library.LibraryIndexMonitor +import com.swordfish.lemuroid.app.shared.library.PendingOperationsMonitor import com.swordfish.lemuroid.app.shared.systems.MetaSystemInfo import com.swordfish.lemuroid.lib.library.GameSystem import com.swordfish.lemuroid.lib.library.db.RetrogradeDatabase @@ -25,7 +25,9 @@ class TVHomeViewModel(retrogradeDb: RetrogradeDatabase, appContext: Context) : V } } - val indexingInProgress = LibraryIndexMonitor(appContext).getLiveData() + val indexingInProgress = PendingOperationsMonitor(appContext).anyLibraryOperationInProgress() + + val directoryScanInProgress = PendingOperationsMonitor(appContext).isDirectoryScanInProgress() val recentGames = retrogradeDb.gameDao().rxSelectFirstUnfavoriteRecents(CAROUSEL_MAX_ITEMS) @@ -34,9 +36,11 @@ class TVHomeViewModel(retrogradeDb: RetrogradeDatabase, appContext: Context) : V val availableSystems: Observable> = retrogradeDb.gameDao() .selectSystemsWithCount() .map { systemCounts -> - systemCounts.filter { (_, count) -> count > 0 } + systemCounts.asSequence().filter { (_, count) -> count > 0 } .map { (systemId, count) -> GameSystem.findById(systemId).metaSystemID() to count } .groupBy { (metaSystemId, _) -> metaSystemId } .map { (metaSystemId, counts) -> MetaSystemInfo(metaSystemId, counts.sumBy { it.second }) } + .sortedBy { it.getName(appContext) } + .toList() } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/home/TVSettingType.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/home/TVSettingType.kt index 3725a8abbc..933fb5cb12 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/home/TVSettingType.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/home/TVSettingType.kt @@ -3,8 +3,10 @@ package com.swordfish.lemuroid.app.tv.home import com.swordfish.lemuroid.R enum class TVSettingType(val icon: Int, val text: Int) { + STOP_RESCAN(R.drawable.ic_stop_white_64dp, R.string.stop), RESCAN(R.drawable.ic_refresh_white_64dp, R.string.rescan), SHOW_ALL_FAVORITES(R.drawable.ic_more_games, R.string.show_all), CHOOSE_DIRECTORY(R.drawable.ic_folder_white_64dp, R.string.directory), SETTINGS(R.drawable.ic_settings_white_64dp, R.string.settings), + SAVE_SYNC(R.drawable.ic_cloud_sync_64dp, R.string.save_sync), } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/main/MainTVActivity.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/main/MainTVActivity.kt index 2883a7e036..3cd75b4a51 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/main/MainTVActivity.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/main/MainTVActivity.kt @@ -12,7 +12,7 @@ import com.swordfish.lemuroid.app.shared.GameInteractor import com.swordfish.lemuroid.app.shared.game.BaseGameActivity import com.swordfish.lemuroid.app.shared.game.GameLauncher import com.swordfish.lemuroid.app.shared.main.BusyActivity -import com.swordfish.lemuroid.app.shared.main.PostGameHandler +import com.swordfish.lemuroid.app.shared.main.GameLaunchTaskHandler import com.swordfish.lemuroid.app.tv.channel.ChannelUpdateWork import com.swordfish.lemuroid.app.tv.favorites.TVFavoritesFragment import com.swordfish.lemuroid.app.tv.games.TVGamesFragment @@ -23,7 +23,7 @@ import com.swordfish.lemuroid.app.tv.shared.TVHelper import com.swordfish.lemuroid.lib.injection.PerActivity import com.swordfish.lemuroid.lib.injection.PerFragment import com.swordfish.lemuroid.lib.library.db.RetrogradeDatabase -import com.swordfish.lemuroid.lib.ui.setVisibleOrGone +import com.swordfish.lemuroid.common.view.setVisibleOrGone import com.tbruyelle.rxpermissions2.RxPermissions import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDispose @@ -37,7 +37,7 @@ import javax.inject.Inject class MainTVActivity : BaseTVActivity(), BusyActivity { - @Inject lateinit var postGameHandler: PostGameHandler + @Inject lateinit var gameLaunchTaskHandler: GameLaunchTaskHandler var mainViewModel: MainTVViewModel? = null @@ -63,7 +63,7 @@ class MainTVActivity : BaseTVActivity(), BusyActivity { when (requestCode) { BaseGameActivity.REQUEST_PLAY_GAME -> { - postGameHandler.handle(false, this, resultCode, data) + gameLaunchTaskHandler.handleGameFinish(false, this, resultCode, data) .andThen(Completable.fromCallable { ChannelUpdateWork.enqueue(applicationContext) }) .subscribeBy(Timber::e) { } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/main/MainTVViewModel.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/main/MainTVViewModel.kt index 783f3d4152..b436dbb7ff 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/main/MainTVViewModel.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/main/MainTVViewModel.kt @@ -3,9 +3,7 @@ package com.swordfish.lemuroid.app.tv.main import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.swordfish.lemuroid.app.shared.library.LibraryIndexMonitor -import com.swordfish.lemuroid.app.shared.savesync.SaveSyncMonitor -import com.swordfish.lemuroid.app.utils.livedata.CombinedLiveData +import com.swordfish.lemuroid.app.shared.library.PendingOperationsMonitor class MainTVViewModel(appContext: Context) : ViewModel() { @@ -15,7 +13,5 @@ class MainTVViewModel(appContext: Context) : ViewModel() { } } - private val indexingInProgress = LibraryIndexMonitor(appContext).getLiveData() - private val saveSyncInProgress = SaveSyncMonitor(appContext).getLiveData() - val inProgress = CombinedLiveData(indexingInProgress, saveSyncInProgress) { a, b -> a || b } + val inProgress = PendingOperationsMonitor(appContext).anyOperationInProgress() } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/search/TVSearchFragment.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/search/TVSearchFragment.kt index 5e0da456af..b266221226 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/search/TVSearchFragment.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/search/TVSearchFragment.kt @@ -14,6 +14,7 @@ import androidx.paging.cachedIn import com.jakewharton.rxrelay2.PublishRelay import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.shared.GameInteractor +import com.swordfish.lemuroid.app.shared.covers.CoverLoader import com.swordfish.lemuroid.app.tv.shared.GamePresenter import com.swordfish.lemuroid.lib.library.db.RetrogradeDatabase import com.swordfish.lemuroid.lib.library.db.entity.Game @@ -28,6 +29,7 @@ class TVSearchFragment : SearchSupportFragment(), SearchSupportFragment.SearchRe @Inject lateinit var retrogradeDb: RetrogradeDatabase @Inject lateinit var gameInteractor: GameInteractor + @Inject lateinit var coverLoader: CoverLoader private val searchRelay: PublishRelay = PublishRelay.create() @@ -72,7 +74,8 @@ class TVSearchFragment : SearchSupportFragment(), SearchSupportFragment.SearchRe val gamePresenter = GamePresenter( resources.getDimensionPixelSize(R.dimen.card_size), - gameInteractor + gameInteractor, + coverLoader ) val gamesAdapter = PagingDataAdapter(gamePresenter, Game.DIFF_CALLBACK) diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/settings/TVSettingsFragment.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/settings/TVSettingsFragment.kt index dcdde90c91..1e13dbc3ee 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/settings/TVSettingsFragment.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/settings/TVSettingsFragment.kt @@ -7,10 +7,11 @@ import androidx.leanback.preference.LeanbackPreferenceFragmentCompat import androidx.preference.Preference import androidx.preference.PreferenceScreen import com.swordfish.lemuroid.R -import com.swordfish.lemuroid.app.shared.savesync.SaveSyncMonitor +import com.swordfish.lemuroid.app.shared.settings.AdvancedSettingsPreferences import com.swordfish.lemuroid.app.shared.settings.BiosPreferences import com.swordfish.lemuroid.app.shared.settings.CoresSelectionPreferences -import com.swordfish.lemuroid.app.shared.settings.GamePadManager +import com.swordfish.lemuroid.app.shared.input.InputDeviceManager +import com.swordfish.lemuroid.app.shared.library.PendingOperationsMonitor import com.swordfish.lemuroid.app.shared.settings.GamePadPreferencesHelper import com.swordfish.lemuroid.app.shared.settings.SaveSyncPreferences import com.swordfish.lemuroid.app.shared.settings.SettingsInteractor @@ -27,7 +28,7 @@ class TVSettingsFragment : LeanbackPreferenceFragmentCompat() { @Inject lateinit var settingsInteractor: SettingsInteractor @Inject lateinit var biosPreferences: BiosPreferences @Inject lateinit var gamePadPreferencesHelper: GamePadPreferencesHelper - @Inject lateinit var gamePadManager: GamePadManager + @Inject lateinit var inputDeviceManager: InputDeviceManager @Inject lateinit var coresSelectionPreferences: CoresSelectionPreferences @Inject lateinit var saveSyncManager: SaveSyncManager @@ -54,6 +55,10 @@ class TVSettingsFragment : LeanbackPreferenceFragmentCompat() { biosPreferences.addBiosPreferences(it) } + getAdvancedSettingsPreferenceScreen()?.let { + AdvancedSettingsPreferences.updateCachePreferences(it) + } + getSaveSyncScreen()?.let { if (saveSyncManager.isSupported()) { saveSyncPreferences.addSaveSyncPreferences(it) @@ -64,7 +69,7 @@ class TVSettingsFragment : LeanbackPreferenceFragmentCompat() { override fun onResume() { super.onResume() - gamePadManager.getGamePadsObservable() + inputDeviceManager.getGamePadsObservable() .distinctUntilChanged() .observeOn(AndroidSchedulers.mainThread()) .autoDispose(scope()) @@ -73,9 +78,11 @@ class TVSettingsFragment : LeanbackPreferenceFragmentCompat() { refreshSaveSyncScreen() getSaveSyncScreen()?.let { screen -> - SaveSyncMonitor(requireContext()).getLiveData().observe(this) { syncInProgress -> - saveSyncPreferences.updatePreferences(screen, syncInProgress) - } + PendingOperationsMonitor(requireContext()) + .anySaveOperationInProgress() + .observe(this) { syncInProgress -> + saveSyncPreferences.updatePreferences(screen, syncInProgress) + } } } @@ -95,6 +102,10 @@ class TVSettingsFragment : LeanbackPreferenceFragmentCompat() { return findPreference(resources.getString(R.string.pref_key_display_bios_info)) } + private fun getAdvancedSettingsPreferenceScreen(): PreferenceScreen? { + return findPreference(resources.getString(R.string.pref_key_advanced_settings)) + } + private fun refreshGamePadBindingsScreen(gamePads: List) { getGamePadPreferenceScreen()?.let { it.removeAll() diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/shared/GamePresenter.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/shared/GamePresenter.kt index ea74f693f3..9f65851df6 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/shared/GamePresenter.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/shared/GamePresenter.kt @@ -13,7 +13,11 @@ import com.swordfish.lemuroid.app.shared.covers.CoverLoader import com.swordfish.lemuroid.app.utils.games.GameUtils import com.swordfish.lemuroid.lib.library.db.entity.Game -class GamePresenter(private val cardSize: Int, private val gameInteractor: GameInteractor) : Presenter() { +class GamePresenter( + private val cardSize: Int, + private val gameInteractor: GameInteractor, + private val coverLoader: CoverLoader +) : Presenter() { override fun onBindViewHolder(viewHolder: Presenter.ViewHolder?, item: Any?) { if (item == null || viewHolder !is ViewHolder) return @@ -21,7 +25,7 @@ class GamePresenter(private val cardSize: Int, private val gameInteractor: GameI viewHolder.mCardView.titleText = game.title viewHolder.mCardView.contentText = GameUtils.getGameSubtitle(viewHolder.mCardView.context, game) viewHolder.mCardView.setMainImageDimensions(cardSize, cardSize) - viewHolder.updateCardViewImage(game) + viewHolder.updateCardViewImage(game, coverLoader) viewHolder.view.setOnCreateContextMenuListener(GameContextMenuListener(gameInteractor, game)) } @@ -36,15 +40,15 @@ class GamePresenter(private val cardSize: Int, private val gameInteractor: GameI override fun onUnbindViewHolder(viewHolder: Presenter.ViewHolder?) { val viewHolder = viewHolder as ViewHolder viewHolder.mCardView.mainImage = null - CoverLoader.cancelRequest(viewHolder.mCardView.mainImageView) + coverLoader.cancelRequest(viewHolder.mCardView.mainImageView) viewHolder.view.setOnCreateContextMenuListener(null) } class ViewHolder(view: ImageCardView) : Presenter.ViewHolder(view) { val mCardView: ImageCardView = view - fun updateCardViewImage(game: Game) { - CoverLoader.loadCover(game, mCardView.mainImageView) + fun updateCardViewImage(game: Game, coverLoader: CoverLoader) { + coverLoader.loadCover(game, mCardView.mainImageView) } } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/shared/TVHelper.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/shared/TVHelper.kt index a72f01f4f5..3dfb130a7c 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/shared/TVHelper.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/shared/TVHelper.kt @@ -1,20 +1,27 @@ package com.swordfish.lemuroid.app.tv.shared import android.content.Context -import android.content.pm.PackageManager +import android.os.Build +import android.os.Environment object TVHelper { fun isSAFSupported(context: Context): Boolean { - val pm: PackageManager = context.packageManager - return !( - pm.hasSystemFeature("android.hardware.type.television") or - pm.hasSystemFeature("android.hardware.type.watch") or - pm.hasSystemFeature("android.hardware.type.automotive") - ) + val packageManager = context.packageManager + + val isStandardHardware = listOf( + !packageManager.hasSystemFeature("android.hardware.type.television"), + !packageManager.hasSystemFeature("android.hardware.type.watch"), + !packageManager.hasSystemFeature("android.hardware.type.automotive"), + ).all { it } + + val isNotLegacyStorage = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !Environment.isExternalStorageLegacy() + + return isStandardHardware || isNotLegacyStorage } fun isTV(context: Context): Boolean { - val pm: PackageManager = context.packageManager - return pm.hasSystemFeature("android.hardware.type.television") + val packageManager = context.packageManager + return packageManager.hasSystemFeature("android.hardware.type.television") } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/utils/livedata/LiveDataUtils.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/utils/livedata/LiveDataUtils.kt new file mode 100644 index 0000000000..af70bbe478 --- /dev/null +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/utils/livedata/LiveDataUtils.kt @@ -0,0 +1,27 @@ +package com.swordfish.lemuroid.app.utils.livedata + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.LiveDataReactiveStreams +import androidx.lifecycle.Transformations +import io.reactivex.Observable + +fun LiveData.combineLatest( + other: LiveData, + combine: (data1: T, data2: K) -> S +): LiveData { + return CombinedLiveData(this, other, combine) +} + +fun LiveData.throttle(delayMs: Long): LiveData { + return ThrottledLiveData(this, delayMs) +} + +fun LiveData.map(mapper: (T) -> K): LiveData { + return Transformations.map(this, mapper) +} + +fun LiveData.toObservable(lifecycleOwner: LifecycleOwner): Observable { + return LiveDataReactiveStreams.toPublisher(lifecycleOwner, this) + .let { Observable.fromPublisher(it) } +} diff --git a/lemuroid-app/src/main/res/drawable/ic_cloud_sync_24dp.xml b/lemuroid-app/src/main/res/drawable/ic_cloud_sync_24dp.xml new file mode 100644 index 0000000000..0b5edff3cd --- /dev/null +++ b/lemuroid-app/src/main/res/drawable/ic_cloud_sync_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/lemuroid-app/src/main/res/drawable/ic_cloud_sync_64dp.xml b/lemuroid-app/src/main/res/drawable/ic_cloud_sync_64dp.xml new file mode 100644 index 0000000000..071f844e71 --- /dev/null +++ b/lemuroid-app/src/main/res/drawable/ic_cloud_sync_64dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/lemuroid-app/src/main/res/drawable/ic_help.xml b/lemuroid-app/src/main/res/drawable/ic_help.xml index 3d4adce1d9..b8f813b4d4 100644 --- a/lemuroid-app/src/main/res/drawable/ic_help.xml +++ b/lemuroid-app/src/main/res/drawable/ic_help.xml @@ -1,4 +1,4 @@ - diff --git a/lemuroid-app/src/main/res/drawable/ic_menu.xml b/lemuroid-app/src/main/res/drawable/ic_menu.xml new file mode 100644 index 0000000000..470db52083 --- /dev/null +++ b/lemuroid-app/src/main/res/drawable/ic_menu.xml @@ -0,0 +1,5 @@ + + + diff --git a/lemuroid-app/src/main/res/drawable/ic_stop_white_64dp.xml b/lemuroid-app/src/main/res/drawable/ic_stop_white_64dp.xml new file mode 100644 index 0000000000..18ae24cb86 --- /dev/null +++ b/lemuroid-app/src/main/res/drawable/ic_stop_white_64dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/lemuroid-app/src/main/res/drawable/ic_sync_white_24dp.xml b/lemuroid-app/src/main/res/drawable/ic_sync_white_24dp.xml new file mode 100644 index 0000000000..f16a1b06eb --- /dev/null +++ b/lemuroid-app/src/main/res/drawable/ic_sync_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/lemuroid-app/src/main/res/drawable/ic_sync_white_64dp.xml b/lemuroid-app/src/main/res/drawable/ic_sync_white_64dp.xml new file mode 100644 index 0000000000..494dc1e259 --- /dev/null +++ b/lemuroid-app/src/main/res/drawable/ic_sync_white_64dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/lemuroid-app/src/main/res/layout/activity_game.xml b/lemuroid-app/src/main/res/layout/activity_game.xml index 07bfe928a6..af21ad51dd 100644 --- a/lemuroid-app/src/main/res/layout/activity_game.xml +++ b/lemuroid-app/src/main/res/layout/activity_game.xml @@ -29,7 +29,8 @@ android:layout_height="match_parent" tools:context=".app.mobile.feature.game.GameActivity" android:keepScreenOn="true" - tools:ignore="MergeRootFrame"> + tools:ignore="MergeRootFrame" + android:background="?attr/colorSurface"> + app:layout_constraintTop_toTopOf="parent" + android:background="#000"/> + android:background="@color/edit_control_overlay"/> + + diff --git a/lemuroid-app/src/main/res/layout/activity_main.xml b/lemuroid-app/src/main/res/layout/activity_main.xml index d6e35e6aad..81e5e3067f 100644 --- a/lemuroid-app/src/main/res/layout/activity_main.xml +++ b/lemuroid-app/src/main/res/layout/activity_main.xml @@ -9,10 +9,9 @@ + android:fitsSystemWindows="true"> - @@ -22,7 +21,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="gone" - android:indeterminate="true" /> + android:indeterminate="true" + android:animateLayoutChanges="true"/> diff --git a/lemuroid-app/src/main/res/layout/activity_tv_main.xml b/lemuroid-app/src/main/res/layout/activity_tv_main.xml index 8d465584ed..f2c4b85fe2 100644 --- a/lemuroid-app/src/main/res/layout/activity_tv_main.xml +++ b/lemuroid-app/src/main/res/layout/activity_tv_main.xml @@ -20,6 +20,6 @@ android:layout_height="wrap_content" android:layout_gravity="top|end" android:indeterminate="true" - android:indeterminateTint="@color/colorPrimaryVariant"/> + android:indeterminateTint="@color/main_color"/> diff --git a/lemuroid-app/src/main/res/layout/layout_game_list.xml b/lemuroid-app/src/main/res/layout/layout_game_list.xml index 47b31c3558..53abf6c8d3 100644 --- a/lemuroid-app/src/main/res/layout/layout_game_list.xml +++ b/lemuroid-app/src/main/res/layout/layout_game_list.xml @@ -67,6 +67,7 @@ android:background="@drawable/favorite_button" android:textOff="" android:textOn="" + app:tint="?attr/colorOnBackground" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/lemuroid-app/src/main/res/layout/layout_game_long_press.xml b/lemuroid-app/src/main/res/layout/layout_game_long_press.xml new file mode 100644 index 0000000000..50dad388e5 --- /dev/null +++ b/lemuroid-app/src/main/res/layout/layout_game_long_press.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/lemuroid-app/src/main/res/layout/layout_system.xml b/lemuroid-app/src/main/res/layout/layout_system.xml index 811061ac83..25dad4de35 100644 --- a/lemuroid-app/src/main/res/layout/layout_system.xml +++ b/lemuroid-app/src/main/res/layout/layout_system.xml @@ -21,6 +21,8 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintDimensionRatio="@string/system_card_image_ratio" + app:tint="?attr/colorOnSurface" + android:tintMode="multiply" android:background="@color/imageview_background" tools:ignore="ContentDescription" /> diff --git a/lemuroid-app/src/main/res/menu/menu_mobile_settings.xml b/lemuroid-app/src/main/res/menu/menu_mobile_settings.xml index bcebb5ca79..61566c30ab 100644 --- a/lemuroid-app/src/main/res/menu/menu_mobile_settings.xml +++ b/lemuroid-app/src/main/res/menu/menu_mobile_settings.xml @@ -1,6 +1,13 @@ + + + + android:label="@string/settings_title_gamepad_settings" /> + + #99000000 + #09ffffff + diff --git a/lemuroid-app/src/main/res/values/core_names.xml b/lemuroid-app/src/main/res/values/core_names.xml index b5721ecf22..0fa36a89bc 100644 --- a/lemuroid-app/src/main/res/values/core_names.xml +++ b/lemuroid-app/src/main/res/values/core_names.xml @@ -19,4 +19,5 @@ dosbox_pure mednafen_ngp mednafen_wswan + citra diff --git a/lemuroid-app/src/main/res/values/dimens.xml b/lemuroid-app/src/main/res/values/dimens.xml index 35a6604e68..59c9342cff 100644 --- a/lemuroid-app/src/main/res/values/dimens.xml +++ b/lemuroid-app/src/main/res/values/dimens.xml @@ -6,4 +6,8 @@ 4:3 1:1 + + 64dp + 32dp + 6dp diff --git a/lemuroid-app/src/main/res/values/keys.xml b/lemuroid-app/src/main/res/values/keys.xml index d634847b5e..4f9a851436 100644 --- a/lemuroid-app/src/main/res/values/keys.xml +++ b/lemuroid-app/src/main/res/values/keys.xml @@ -1,6 +1,7 @@ rescan + stop_rescan display_bios_info advanced_settings open_cores_selection @@ -13,6 +14,10 @@ autosave reset_settings low_latency_audio + enable_rumble + enable_device_rumble + max_cache_size_bytes + allow_direct_game_load save_sync_auto save_sync_enable diff --git a/lemuroid-app/src/main/res/values/leanback-colors.xml b/lemuroid-app/src/main/res/values/leanback-colors.xml new file mode 100644 index 0000000000..8a88477a2c --- /dev/null +++ b/lemuroid-app/src/main/res/values/leanback-colors.xml @@ -0,0 +1,5 @@ + + @color/surface_elevation_0dp + @color/surface_elevation_3dp + @color/surface_elevation_1dp + diff --git a/lemuroid-app/src/main/res/values/leanback-themes.xml b/lemuroid-app/src/main/res/values/leanback-themes.xml new file mode 100644 index 0000000000..6c9ce7a07a --- /dev/null +++ b/lemuroid-app/src/main/res/values/leanback-themes.xml @@ -0,0 +1,29 @@ + + + + @color/leanback_foreground + @color/leanback_info + + + + + + + diff --git a/lemuroid-app/src/main/res/values/material-colors.xml b/lemuroid-app/src/main/res/values/material-colors.xml new file mode 100644 index 0000000000..0d9dd5c0b0 --- /dev/null +++ b/lemuroid-app/src/main/res/values/material-colors.xml @@ -0,0 +1,62 @@ + + #00c64e + #9de3aa + + + #006E24 + #FFFFFF + #6AFF86 + #002106 + #516350 + #FFFFFF + #D4E8D0 + #0F1F10 + #39656C + #FFFFFF + #BDEBF3 + #001F23 + #BA1B1B + #FFDAD4 + #FFFFFF + #410001 + #FCFDF7 + #1A1C19 + #FCFDF7 + #1A1C19 + #DEE5D9 + #424840 + #72796F + #F0F1EB + #2E312D + #3EE266 + #000000 + #3EE266 + #3EE266 + #00390E + #005319 + #6AFF86 + #B9CCB5 + #243424 + #3A4B39 + #D4E8D0 + #A0CED5 + #00363C + #1F4D53 + #BDEBF3 + #FFB4A9 + #930006 + #680003 + #FFDAD4 + #1A1C19 + #E2E3DD + #1A1C19 + #E2E3DD + #424840 + #C1C9BD + #8B9388 + #1A1C19 + #E2E3DD + #006E24 + #000000 + #006E24 + diff --git a/lemuroid-app/src/main/res/values/material-themes.xml b/lemuroid-app/src/main/res/values/material-themes.xml new file mode 100644 index 0000000000..1a56862294 --- /dev/null +++ b/lemuroid-app/src/main/res/values/material-themes.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + diff --git a/lemuroid-app/src/main/res/values/strings.xml b/lemuroid-app/src/main/res/values/strings.xml index eae0428f67..ab5f700aee 100644 --- a/lemuroid-app/src/main/res/values/strings.xml +++ b/lemuroid-app/src/main/res/values/strings.xml @@ -8,6 +8,7 @@ Favorites Discover Rescan + Stop None Directory Display filter @@ -15,6 +16,7 @@ OK Cancel Settings + Save Sync Show All General @@ -23,13 +25,16 @@ Autosave state Vibrate on touch Tilt sensor sensitivity + Cache size limit + + External devices + Configure external gamepads and keyboards - Gamepad settings Button %1$s RetroPad %1$s Unassigned - Enabled Gamepads + Enabled devices General Reset bindings Menu @@ -50,6 +55,15 @@ Prefer low-latency audio Reduce audio latency on supported devices. Might increase audio glitches. + Direct game load + (Experimental) Reduce loading time and cache usage on supported consoles. + + Rumble + Enable rumble in supported games and controllers + + Device rumble + When no controller is connected vibrate the device instead + Are you sure? This will reset Lemuroid to its factory settings. Saves and States will be preserved. @@ -104,6 +118,7 @@ All notifications ROMs scanning in progress This can take up to a few minutes + @string/cancel Cloud save Sync in progress This can take up to a few minutes diff --git a/lemuroid-app/src/main/res/values/styles.xml b/lemuroid-app/src/main/res/values/styles.xml deleted file mode 100644 index 35ee0f20d5..0000000000 --- a/lemuroid-app/src/main/res/values/styles.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - diff --git a/lemuroid-app/src/main/res/xml/mobile_settings.xml b/lemuroid-app/src/main/res/xml/mobile_settings.xml index e95605b83d..6e9a5ffc3f 100644 --- a/lemuroid-app/src/main/res/xml/mobile_settings.xml +++ b/lemuroid-app/src/main/res/xml/mobile_settings.xml @@ -19,6 +19,13 @@ app:iconSpaceReserved="false" android:persistent="false"/> + + diff --git a/lemuroid-app/src/main/res/xml/mobile_settings_advanced.xml b/lemuroid-app/src/main/res/xml/mobile_settings_advanced.xml index cdedf10d9c..a9b185dd76 100644 --- a/lemuroid-app/src/main/res/xml/mobile_settings_advanced.xml +++ b/lemuroid-app/src/main/res/xml/mobile_settings_advanced.xml @@ -2,25 +2,64 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/lemuroid-app/src/main/res/xml/tv_settings.xml b/lemuroid-app/src/main/res/xml/tv_settings.xml index 9f04291b45..5ec92ec011 100644 --- a/lemuroid-app/src/main/res/xml/tv_settings.xml +++ b/lemuroid-app/src/main/res/xml/tv_settings.xml @@ -30,8 +30,9 @@ + android:title="@string/settings_title_gamepad_settings"/> @@ -63,18 +64,49 @@ android:summary="@string/settings_description_advanced_settings" app:iconSpaceReserved="false"> - - - + + + + + + + + + + + + + + + + + diff --git a/lemuroid-cores b/lemuroid-cores index c2b0de0e4c..e5c3b4c45d 160000 --- a/lemuroid-cores +++ b/lemuroid-cores @@ -1 +1 @@ -Subproject commit c2b0de0e4cf0ffa5207caa8897bd41480418f4f0 +Subproject commit e5c3b4c45da24f2a9c5e7b31c2dca3add5363460 diff --git a/lemuroid-metadata-libretro-db/build.gradle.kts b/lemuroid-metadata-libretro-db/build.gradle.kts index 4d9069e1cb..42e780ceb7 100644 --- a/lemuroid-metadata-libretro-db/build.gradle.kts +++ b/lemuroid-metadata-libretro-db/build.gradle.kts @@ -22,4 +22,7 @@ dependencies { android { resourcePrefix("libretrodb_") + kotlinOptions { + jvmTarget = "1.8" + } } diff --git a/lemuroid-metadata-libretro-db/src/main/assets/libretro-db.sqlite b/lemuroid-metadata-libretro-db/src/main/assets/libretro-db.sqlite index 7fb5297188..b9e28f880e 100644 Binary files a/lemuroid-metadata-libretro-db/src/main/assets/libretro-db.sqlite and b/lemuroid-metadata-libretro-db/src/main/assets/libretro-db.sqlite differ diff --git a/lemuroid-metadata-libretro-db/src/main/java/com/swordfish/lemuroid/metadata/libretrodb/db/LibretroDatabase.kt b/lemuroid-metadata-libretro-db/src/main/java/com/swordfish/lemuroid/metadata/libretrodb/db/LibretroDatabase.kt index d85cc64ff2..e3bd7da21a 100644 --- a/lemuroid-metadata-libretro-db/src/main/java/com/swordfish/lemuroid/metadata/libretrodb/db/LibretroDatabase.kt +++ b/lemuroid-metadata-libretro-db/src/main/java/com/swordfish/lemuroid/metadata/libretrodb/db/LibretroDatabase.kt @@ -7,7 +7,7 @@ import com.swordfish.lemuroid.metadata.libretrodb.db.entity.LibretroRom @Database( entities = [LibretroRom::class], - version = 7, + version = 8, exportSchema = false ) abstract class LibretroDatabase : RoomDatabase() { diff --git a/lemuroid-touchinput/build.gradle.kts b/lemuroid-touchinput/build.gradle.kts index 5416314864..a11884c351 100644 --- a/lemuroid-touchinput/build.gradle.kts +++ b/lemuroid-touchinput/build.gradle.kts @@ -4,6 +4,12 @@ plugins { id("kotlin-kapt") } +android { + kotlinOptions { + jvmTarget = "1.8" + } +} + dependencies { implementation(project(":retrograde-util")) diff --git a/lemuroid-touchinput/src/main/java/com/swordfish/touchinput/radial/RadialPadConfigs.kt b/lemuroid-touchinput/src/main/java/com/swordfish/touchinput/radial/LemuroidTouchConfigs.kt similarity index 72% rename from lemuroid-touchinput/src/main/java/com/swordfish/touchinput/radial/RadialPadConfigs.kt rename to lemuroid-touchinput/src/main/java/com/swordfish/touchinput/radial/LemuroidTouchConfigs.kt index 92b0fb8abc..398e91c4aa 100644 --- a/lemuroid-touchinput/src/main/java/com/swordfish/touchinput/radial/RadialPadConfigs.kt +++ b/lemuroid-touchinput/src/main/java/com/swordfish/touchinput/radial/LemuroidTouchConfigs.kt @@ -1,15 +1,143 @@ package com.swordfish.touchinput.radial import android.view.KeyEvent +import android.view.View import com.swordfish.radialgamepad.library.config.ButtonConfig +import com.swordfish.radialgamepad.library.config.CrossConfig import com.swordfish.radialgamepad.library.config.RadialGamePadConfig import com.swordfish.radialgamepad.library.config.PrimaryDialConfig import com.swordfish.radialgamepad.library.config.SecondaryDialConfig import com.swordfish.radialgamepad.library.config.CrossContentDescription +import com.swordfish.radialgamepad.library.config.RadialGamePadTheme import com.swordfish.radialgamepad.library.event.GestureType +import com.swordfish.radialgamepad.library.haptics.HapticConfig import com.swordfish.touchinput.controller.R -object RadialPadConfigs { +object LemuroidTouchConfigs { + + enum class Kind { + GB_LEFT, + GB_RIGHT, + NES_LEFT, + NES_RIGHT, + DESMUME_LEFT, + DESMUME_RIGHT, + MELONDS_NDS_LEFT, + MELONDS_NDS_RIGHT, + PSX_LEFT, + PSX_RIGHT, + PSX_DUALSHOCK_LEFT, + PSX_DUALSHOCK_RIGHT, + PSP_LEFT, + PSP_RIGHT, + SNES_LEFT, + SNES_RIGHT, + GBA_LEFT, + GBA_RIGHT, + SMS_LEFT, + SMS_RIGHT, + GG_LEFT, + GG_RIGHT, + LYNX_LEFT, + LYNX_RIGHT, + PCE_LEFT, + PCE_RIGHT, + DOS_LEFT, + DOS_RIGHT, + NGP_LEFT, + NGP_RIGHT, + WS_LANDSCAPE_LEFT, + WS_LANDSCAPE_RIGHT, + WS_PORTRAIT_LEFT, + WS_PORTRAIT_RIGHT, + N64_LEFT, + N64_RIGHT, + GENESIS_3_LEFT, + GENESIS_3_RIGHT, + GENESIS_6_LEFT, + GENESIS_6_RIGHT, + ATARI2600_LEFT, + ATARI2600_RIGHT, + ARCADE_4_LEFT, + ARCADE_4_RIGHT, + ARCADE_6_LEFT, + ARCADE_6_RIGHT, + ATARI7800_LEFT, + ATARI7800_RIGHT, + NINTENDO_3DS_LEFT, + NINTENDO_3DS_RIGHT + } + + private data class Config( + val standardTheme: RadialGamePadTheme, + val alternateTheme: RadialGamePadTheme + ) + + fun getRadialGamePadConfig( + kind: Kind, + haptic: HapticConfig, + view: View + ): RadialGamePadConfig { + val config = Config( + LemuroidTouchOverlayThemes.getGamePadTheme(view), + LemuroidTouchOverlayThemes.getGamePadAlternate(view) + ) + + val radialGamePadConfig = when (kind) { + Kind.GB_LEFT -> getGBLeft(config) + Kind.GB_RIGHT -> getGBRight(config) + Kind.NES_LEFT -> getNESLeft(config) + Kind.NES_RIGHT -> getNESRight(config) + Kind.DESMUME_LEFT -> getDesmumeLeft(config) + Kind.DESMUME_RIGHT -> getDesmumeRight(config) + Kind.MELONDS_NDS_LEFT -> getMelondsLeft(config) + Kind.MELONDS_NDS_RIGHT -> getMelondsRight(config) + Kind.PSX_LEFT -> getPSXLeft(config) + Kind.PSX_RIGHT -> getPSXRight(config) + Kind.PSX_DUALSHOCK_LEFT -> getPSXDualshockLeft(config) + Kind.PSX_DUALSHOCK_RIGHT -> getPSXDualshockRight(config) + Kind.PSP_LEFT -> getPSPLeft(config) + Kind.PSP_RIGHT -> getPSPRight(config) + Kind.SNES_LEFT -> getSNESLeft(config) + Kind.SNES_RIGHT -> getSNESRight(config) + Kind.GBA_LEFT -> getGBALeft(config) + Kind.GBA_RIGHT -> getGBARight(config) + Kind.SMS_LEFT -> getSMSLeft(config) + Kind.SMS_RIGHT -> getSMSRight(config) + Kind.GG_LEFT -> getGGLeft(config) + Kind.GG_RIGHT -> getGGRight(config) + Kind.LYNX_LEFT -> getLynxLeft(config) + Kind.LYNX_RIGHT -> getLynxRight(config) + Kind.PCE_LEFT -> getPCELeft(config) + Kind.PCE_RIGHT -> getPCERight(config) + Kind.DOS_LEFT -> getDOSLeft(config) + Kind.DOS_RIGHT -> getDOSRight(config) + Kind.NGP_LEFT -> getNGPLeft(config) + Kind.NGP_RIGHT -> getNGPRight(config) + Kind.WS_LANDSCAPE_LEFT -> getWSLandscapeLeft(config) + Kind.WS_LANDSCAPE_RIGHT -> getWSLandscapeRight(config) + Kind.WS_PORTRAIT_LEFT -> getWSPortraitLeft(config) + Kind.WS_PORTRAIT_RIGHT -> getWSPortraitRight(config) + Kind.N64_LEFT -> getN64Left(config) + Kind.N64_RIGHT -> getN64Right(config) + Kind.GENESIS_3_LEFT -> getGenesis3Left(config) + Kind.GENESIS_3_RIGHT -> getGenesis3Right(config) + Kind.GENESIS_6_LEFT -> getGenesis6Left(config) + Kind.GENESIS_6_RIGHT -> getGenesis6Right(config) + Kind.ATARI2600_LEFT -> getAtari2600Left(config) + Kind.ATARI2600_RIGHT -> getAtari2600Right(config) + Kind.ARCADE_4_LEFT -> getArcade4Left(config) + Kind.ARCADE_4_RIGHT -> getArcade4Right(config) + Kind.ARCADE_6_LEFT -> getArcade6Left(config) + Kind.ARCADE_6_RIGHT -> getArcade6Right(config) + Kind.ATARI7800_LEFT -> getAtari7800Left(config) + Kind.ATARI7800_RIGHT -> getAtari7800Right(config) + Kind.NINTENDO_3DS_LEFT -> getNintendo3DSLeft(config) + Kind.NINTENDO_3DS_RIGHT -> getNintendo3DSRight(config) + } + + return radialGamePadConfig.copy(haptic = haptic) + } const val MOTION_SOURCE_DPAD = 0 const val MOTION_SOURCE_LEFT_STICK = 1 @@ -32,9 +160,7 @@ object RadialPadConfigs { private val BUTTON_CONFIG_MENU = ButtonConfig( id = KeyEvent.KEYCODE_BUTTON_MODE, iconId = R.drawable.button_menu, - contentDescription = "Menu", - supportsGestures = setOf(GestureType.FIRST_TOUCH), - supportsButtons = false + contentDescription = "Menu" ) private val BUTTON_CONFIG_CROSS = ButtonConfig( @@ -104,17 +230,24 @@ object RadialPadConfigs { ) private val PRIMARY_DIAL_CROSS = PrimaryDialConfig.Cross( - MOTION_SOURCE_DPAD, - supportsGestures = setOf(GestureType.TRIPLE_TAP, GestureType.FIRST_TOUCH) + CrossConfig( + id = MOTION_SOURCE_DPAD, + shape = CrossConfig.Shape.STANDARD, + supportsGestures = setOf(GestureType.TRIPLE_TAP, GestureType.FIRST_TOUCH) + ), ) private val PRIMARY_DIAL_CROSS_MERGED = PrimaryDialConfig.Cross( - MOTION_SOURCE_DPAD_AND_LEFT_STICK, - supportsGestures = setOf(GestureType.TRIPLE_TAP, GestureType.FIRST_TOUCH) + CrossConfig( + id = MOTION_SOURCE_DPAD_AND_LEFT_STICK, + shape = CrossConfig.Shape.STANDARD, + supportsGestures = setOf(GestureType.TRIPLE_TAP, GestureType.FIRST_TOUCH) + ) ) - val GB_LEFT = + private fun getGBLeft(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( @@ -123,8 +256,9 @@ object RadialPadConfigs { ) ) - val GB_RIGHT = + private fun getGBRight(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( dials = listOf( @@ -141,22 +275,24 @@ object RadialPadConfigs { ), secondaryDials = listOf( SecondaryDialConfig.SingleButton(2, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val NES_LEFT = + private fun getNESLeft(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( SecondaryDialConfig.SingleButton(4, 1, BUTTON_CONFIG_SELECT), - SecondaryDialConfig.Empty(10, 1, 1f) + SecondaryDialConfig.Empty(8, 1, 1f) ) ) - val NES_RIGHT = + private fun getNESRight(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( dials = listOf( @@ -172,12 +308,13 @@ object RadialPadConfigs { ), secondaryDials = listOf( SecondaryDialConfig.SingleButton(2, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val DESMUME_LEFT = + private fun getDesmumeLeft(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( @@ -204,7 +341,8 @@ object RadialPadConfigs { ) ) - val DESMUME_RIGHT = RadialGamePadConfig( + private fun getDesmumeRight(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( dials = listOf( @@ -229,12 +367,13 @@ object RadialPadConfigs { secondaryDials = listOf( SecondaryDialConfig.SingleButton(2, 1, BUTTON_CONFIG_R), SecondaryDialConfig.SingleButton(4, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val MELONDS_NDS_LEFT = + private fun getMelondsLeft(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( @@ -261,7 +400,8 @@ object RadialPadConfigs { ) ) - val MELONDS_NDS_RIGHT = RadialGamePadConfig( + private fun getMelondsRight(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( dials = listOf( @@ -286,12 +426,13 @@ object RadialPadConfigs { secondaryDials = listOf( SecondaryDialConfig.SingleButton(2, 1, BUTTON_CONFIG_R), SecondaryDialConfig.SingleButton(4, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val PSX_LEFT = + private fun getPSXLeft(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( @@ -302,8 +443,9 @@ object RadialPadConfigs { ) ) - val PSX_RIGHT = + private fun getPSXRight(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( listOf( @@ -317,12 +459,13 @@ object RadialPadConfigs { SecondaryDialConfig.SingleButton(2, 1, BUTTON_CONFIG_R2), SecondaryDialConfig.SingleButton(3, 1, BUTTON_CONFIG_R1), SecondaryDialConfig.SingleButton(4, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val PSX_DUALSHOCK_LEFT = + private fun getPSXDualshockLeft(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( @@ -341,8 +484,9 @@ object RadialPadConfigs { ) ) - val PSX_DUALSHOCK_RIGHT = + private fun getPSXDualshockRight(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( listOf( @@ -364,12 +508,13 @@ object RadialPadConfigs { contentDescription = "Right Stick", supportsGestures = setOf(GestureType.TRIPLE_TAP, GestureType.FIRST_TOUCH) ), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val N64_LEFT = + private fun getN64Left(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( @@ -393,8 +538,9 @@ object RadialPadConfigs { ) ) - val N64_RIGHT = + private fun getN64Right(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( listOf( @@ -419,21 +565,24 @@ object RadialPadConfigs { SecondaryDialConfig.Cross( 8, 2.2f, - MOTION_SOURCE_RIGHT_DPAD, - R.drawable.direction_alt_background, - R.drawable.direction_alt_foreground, - contentDescription = CrossContentDescription( - baseName = "c" - ), - supportsGestures = setOf(GestureType.TRIPLE_TAP, GestureType.FIRST_TOUCH), - useDiagonals = false + CrossConfig( + id = MOTION_SOURCE_RIGHT_DPAD, + shape = CrossConfig.Shape.CIRCLE, + rightDrawableForegroundId = R.drawable.direction_alt_foreground, + contentDescription = CrossContentDescription( + baseName = "c" + ), + supportsGestures = setOf(GestureType.TRIPLE_TAP, GestureType.FIRST_TOUCH), + useDiagonals = false + ) ), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val PSP_LEFT = + private fun getPSPLeft(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( @@ -449,8 +598,9 @@ object RadialPadConfigs { ) ) - val PSP_RIGHT = + private fun getPSPRight(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( listOf( @@ -464,12 +614,13 @@ object RadialPadConfigs { SecondaryDialConfig.SingleButton(2, 2, BUTTON_CONFIG_R), SecondaryDialConfig.SingleButton(4, 1, BUTTON_CONFIG_START), SecondaryDialConfig.Empty(8, 2, 2.2f), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val SNES_LEFT = + private fun getSNESLeft(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( @@ -479,8 +630,9 @@ object RadialPadConfigs { ) ) - val SNES_RIGHT = + private fun getSNESRight(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( dials = listOf( @@ -505,12 +657,13 @@ object RadialPadConfigs { secondaryDials = listOf( SecondaryDialConfig.SingleButton(2, 2, BUTTON_CONFIG_R), SecondaryDialConfig.SingleButton(4, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val GBA_LEFT = + private fun getGBALeft(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( @@ -520,8 +673,9 @@ object RadialPadConfigs { ) ) - val GBA_RIGHT = + private fun getGBARight(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( dials = listOf( @@ -539,22 +693,24 @@ object RadialPadConfigs { secondaryDials = listOf( SecondaryDialConfig.SingleButton(2, 2, BUTTON_CONFIG_R), SecondaryDialConfig.SingleButton(4, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val GENESIS_3_LEFT = + private fun getGenesis3Left(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( SecondaryDialConfig.SingleButton(4, 1, BUTTON_CONFIG_SELECT), - SecondaryDialConfig.Empty(10, 1, 1f) + SecondaryDialConfig.Empty(8, 1, 1f) ) ) - val GENESIS_3_RIGHT = + private fun getGenesis3Right(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( dials = listOf( @@ -574,24 +730,26 @@ object RadialPadConfigs { ), secondaryDials = listOf( SecondaryDialConfig.SingleButton(2, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val GENESIS_6_LEFT = + private fun getGenesis6Left(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( SecondaryDialConfig.SingleButton(4, 1, BUTTON_CONFIG_SELECT), SecondaryDialConfig.SingleButton(3, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(8, 1, BUTTON_CONFIG_MENU), + buildMenuButtonConfig(8, config), SecondaryDialConfig.Empty(9, 1, 1f) ) ) - val GENESIS_6_RIGHT = + private fun getGenesis6Right(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( dials = listOf( @@ -631,8 +789,9 @@ object RadialPadConfigs { ) ) - val ATARI2600_LEFT = + private fun getAtari2600Left(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 10, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( @@ -656,8 +815,9 @@ object RadialPadConfigs { ) ) - val ATARI2600_RIGHT = + private fun getAtari2600Right(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 10, primaryDial = PrimaryDialConfig.PrimaryButtons( dials = listOf(), @@ -683,12 +843,13 @@ object RadialPadConfigs { label = "SELECT" ) ), - SecondaryDialConfig.SingleButton(8, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(8, config) ) ) - val SMS_LEFT = + private fun getSMSLeft(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( @@ -697,8 +858,9 @@ object RadialPadConfigs { ) ) - val SMS_RIGHT = + private fun getSMSRight(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( dials = listOf( @@ -714,12 +876,13 @@ object RadialPadConfigs { ), secondaryDials = listOf( SecondaryDialConfig.SingleButton(2, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val GG_LEFT = + private fun getGGLeft(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( @@ -728,8 +891,9 @@ object RadialPadConfigs { ) ) - val GG_RIGHT = + private fun getGGRight(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( dials = listOf( @@ -746,22 +910,24 @@ object RadialPadConfigs { ), secondaryDials = listOf( SecondaryDialConfig.SingleButton(2, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val ARCADE_4_LEFT = + private fun getArcade4Left(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS_MERGED, secondaryDials = listOf( SecondaryDialConfig.SingleButton(4, 1, BUTTON_CONFIG_COIN), - SecondaryDialConfig.Empty(10, 1, 1f) + SecondaryDialConfig.Empty(8, 1, 1f) ) ) - val ARCADE_4_RIGHT = + private fun getArcade4Right(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( rotationInDegrees = 60f, @@ -786,24 +952,26 @@ object RadialPadConfigs { ), secondaryDials = listOf( SecondaryDialConfig.SingleButton(2, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val ARCADE_6_LEFT = + private fun getArcade6Left(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS_MERGED, secondaryDials = listOf( SecondaryDialConfig.SingleButton(4, 1, BUTTON_CONFIG_COIN), SecondaryDialConfig.SingleButton(3, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(8, 1, BUTTON_CONFIG_MENU), + buildMenuButtonConfig(8, config), SecondaryDialConfig.Empty(9, 1, 1f) ) ) - val ARCADE_6_RIGHT = + private fun getArcade6Right(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( dials = listOf( @@ -842,8 +1010,9 @@ object RadialPadConfigs { ) ) - val LYNX_LEFT = + private fun getLynxLeft(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( @@ -863,12 +1032,13 @@ object RadialPadConfigs { label = "OPTION 2" ) ), - SecondaryDialConfig.Empty(10, 1, 1f) + SecondaryDialConfig.Empty(8, 1, 1f) ) ) - val LYNX_RIGHT = + private fun getLynxRight(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( rotationInDegrees = 15f, @@ -885,22 +1055,24 @@ object RadialPadConfigs { ), secondaryDials = listOf( SecondaryDialConfig.SingleButton(2, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val ATARI7800_LEFT = + private fun getAtari7800Left(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( SecondaryDialConfig.SingleButton(4, 1, BUTTON_CONFIG_SELECT), - SecondaryDialConfig.Empty(10, 1, 1f) + SecondaryDialConfig.Empty(8, 1, 1f) ) ) - val ATARI7800_RIGHT = + private fun getAtari7800Right(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( dials = listOf( @@ -916,22 +1088,24 @@ object RadialPadConfigs { ), secondaryDials = listOf( SecondaryDialConfig.SingleButton(2, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val PCE_LEFT = + private fun getPCELeft(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( SecondaryDialConfig.SingleButton(4, 1, BUTTON_CONFIG_SELECT), - SecondaryDialConfig.Empty(10, 1, 1f) + SecondaryDialConfig.Empty(8, 1, 1f) ) ) - val PCE_RIGHT = + private fun getPCERight(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( dials = listOf( @@ -947,12 +1121,13 @@ object RadialPadConfigs { ), secondaryDials = listOf( SecondaryDialConfig.SingleButton(2, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val DOS_LEFT = + private fun getDOSLeft(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( @@ -1000,8 +1175,9 @@ object RadialPadConfigs { ) ) - val DOS_RIGHT = + private fun getDOSRight(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( listOf( @@ -1055,22 +1231,24 @@ object RadialPadConfigs { contentDescription = "Right Stick", supportsGestures = setOf(GestureType.TRIPLE_TAP, GestureType.FIRST_TOUCH) ), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val NGP_LEFT = + private fun getNGPLeft(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( SecondaryDialConfig.Empty(4, 1, 1f), - SecondaryDialConfig.Empty(10, 1, 1f) + SecondaryDialConfig.Empty(8, 1, 1f) ) ) - val NGP_RIGHT = + private fun getNGPRight(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( dials = listOf( @@ -1086,22 +1264,24 @@ object RadialPadConfigs { ), secondaryDials = listOf( SecondaryDialConfig.SingleButton(2, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val WS_LANDSCAPE_LEFT = + private fun getWSLandscapeLeft(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( SecondaryDialConfig.Empty(4, 1, 1f), - SecondaryDialConfig.Empty(10, 1, 1f) + SecondaryDialConfig.Empty(8, 1, 1f) ) ) - val WS_LANDSCAPE_RIGHT = + private fun getWSLandscapeRight(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( rotationInDegrees = 30f, @@ -1118,22 +1298,24 @@ object RadialPadConfigs { ), secondaryDials = listOf( SecondaryDialConfig.SingleButton(2, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) ) ) - val WS_PORTRAIT_LEFT = + private fun getWSPortraitLeft(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PRIMARY_DIAL_CROSS, secondaryDials = listOf( SecondaryDialConfig.Empty(4, 1, 1f), - SecondaryDialConfig.Empty(10, 1, 1f) + SecondaryDialConfig.Empty(8, 1, 1f) ) ) - val WS_PORTRAIT_RIGHT = + private fun getWSPortraitRight(config: Config) = RadialGamePadConfig( + theme = config.standardTheme, sockets = 12, primaryDial = PrimaryDialConfig.PrimaryButtons( dials = listOf( @@ -1157,7 +1339,67 @@ object RadialPadConfigs { ), secondaryDials = listOf( SecondaryDialConfig.SingleButton(2, 1, BUTTON_CONFIG_START), - SecondaryDialConfig.SingleButton(10, 1, BUTTON_CONFIG_MENU) + buildMenuButtonConfig(10, config) + ) + ) + + private fun getNintendo3DSLeft(config: Config) = + RadialGamePadConfig( + theme = config.standardTheme, + sockets = 12, + primaryDial = PRIMARY_DIAL_CROSS, + secondaryDials = listOf( + SecondaryDialConfig.SingleButton(2, 1, BUTTON_CONFIG_SELECT), + SecondaryDialConfig.SingleButton(3, 2, BUTTON_CONFIG_L), + SecondaryDialConfig.Stick( + 9, + 2.2f, + MOTION_SOURCE_LEFT_STICK, + supportsGestures = setOf(GestureType.TRIPLE_TAP, GestureType.FIRST_TOUCH) + ), + SecondaryDialConfig.Empty(8, 1, 1f) ) ) + + private fun getNintendo3DSRight(config: Config) = + RadialGamePadConfig( + theme = config.standardTheme, + sockets = 12, + primaryDial = PrimaryDialConfig.PrimaryButtons( + dials = listOf( + ButtonConfig( + id = KeyEvent.KEYCODE_BUTTON_A, + label = "A" + ), + ButtonConfig( + id = KeyEvent.KEYCODE_BUTTON_X, + label = "X" + ), + ButtonConfig( + id = KeyEvent.KEYCODE_BUTTON_Y, + label = "Y" + ), + ButtonConfig( + id = KeyEvent.KEYCODE_BUTTON_B, + label = "B" + ) + ) + ), + secondaryDials = listOf( + SecondaryDialConfig.SingleButton(2, 2, BUTTON_CONFIG_R), + SecondaryDialConfig.SingleButton(4, 1, BUTTON_CONFIG_START), + SecondaryDialConfig.Empty(8, 2, 2.2f), + buildMenuButtonConfig(10, config) + ) + ) + + private fun buildMenuButtonConfig(index: Int, config: Config): SecondaryDialConfig { + return SecondaryDialConfig.SingleButton( + index = index, + spread = 1, + buttonConfig = BUTTON_CONFIG_MENU, + processSecondaryDialRotation = { -it }, + theme = config.alternateTheme + ) + } } diff --git a/lemuroid-touchinput/src/main/java/com/swordfish/touchinput/radial/LemuroidTouchOverlayThemes.kt b/lemuroid-touchinput/src/main/java/com/swordfish/touchinput/radial/LemuroidTouchOverlayThemes.kt new file mode 100644 index 0000000000..eb1e6b8762 --- /dev/null +++ b/lemuroid-touchinput/src/main/java/com/swordfish/touchinput/radial/LemuroidTouchOverlayThemes.kt @@ -0,0 +1,49 @@ +package com.swordfish.touchinput.radial + +import android.view.View +import androidx.core.graphics.ColorUtils +import com.google.android.material.color.MaterialColors +import com.swordfish.radialgamepad.library.config.RadialGamePadTheme +import com.google.android.material.R + +object LemuroidTouchOverlayThemes { + + private const val BASE_TRANSPARENCY = 0.6f + + fun getGamePadTheme(view: View): RadialGamePadTheme { + val colorOnSurface = MaterialColors.getColor(view, R.attr.colorOnSurface) + val colorSurface = MaterialColors.getColor(view, R.attr.colorSurface) + val colorPrimary = MaterialColors.getColor(view, R.attr.colorPrimary) + val colorIntermediate = ColorUtils.blendARGB(colorSurface, colorOnSurface, 0.5f) + + return RadialGamePadTheme( + normalColor = withAlpha(colorIntermediate, 0.75f), + primaryDialBackground = withAlpha(colorIntermediate, 0.25f), + pressedColor = withAlpha(colorPrimary, 1f), + textColor = withAlpha(colorOnSurface, 1f), + simulatedColor = withAlpha(colorPrimary, 0.75f), + lightColor = withAlpha(colorOnSurface, 0.25f), + ) + } + + fun getGamePadAlternate(view: View): RadialGamePadTheme { + val colorOnSurface = MaterialColors.getColor(view, R.attr.colorOnSurface) + val colorSurface = MaterialColors.getColor(view, R.attr.colorSurface) + val colorPrimary = MaterialColors.getColor(view, R.attr.colorPrimary) + val colorIntermediate = ColorUtils.blendARGB(colorSurface, colorOnSurface, 0.25f) + + return RadialGamePadTheme( + normalColor = withAlpha(colorIntermediate, 0.75f), + primaryDialBackground = withAlpha(colorIntermediate, 0.25f), + pressedColor = withAlpha(colorPrimary, 1f), + textColor = withAlpha(colorOnSurface, 1f), + simulatedColor = withAlpha(colorPrimary, 0.75f), + lightColor = withAlpha(colorOnSurface, 0.25f), + ) + } + + private fun withAlpha(color: Int, alpha: Float): Int { + val alphaInt = (alpha * 255 * BASE_TRANSPARENCY).toInt() + return MaterialColors.compositeARGBWithAlpha(color, alphaInt) + } +} diff --git a/lemuroid-touchinput/src/main/res/drawable/button_menu.xml b/lemuroid-touchinput/src/main/res/drawable/button_menu.xml index b0435ccd28..717623c83b 100644 --- a/lemuroid-touchinput/src/main/res/drawable/button_menu.xml +++ b/lemuroid-touchinput/src/main/res/drawable/button_menu.xml @@ -3,7 +3,9 @@ android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0"> + + android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z"/> + diff --git a/lemuroid-touchinput/src/main/res/drawable/direction_alt_background.xml b/lemuroid-touchinput/src/main/res/drawable/direction_alt_background.xml deleted file mode 100644 index a7a9b6d09a..0000000000 --- a/lemuroid-touchinput/src/main/res/drawable/direction_alt_background.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/lemuroid-touchinput/src/main/res/values/colors.xml b/lemuroid-touchinput/src/main/res/values/colors.xml index eee7338992..45e0f8366c 100644 --- a/lemuroid-touchinput/src/main/res/values/colors.xml +++ b/lemuroid-touchinput/src/main/res/values/colors.xml @@ -1,6 +1,11 @@ - #7fff - #7777 - #3777 + #80FFFFFF + #80808080 + #10FFFFFF + #08FFFFFF + #08FFFFFF + + #80404040 + #08FFFFFF diff --git a/lemuroid-touchinput/src/main/res/values/dimen.xml b/lemuroid-touchinput/src/main/res/values/dimen.xml index 7cd7d7b7c2..e1a427504b 100644 --- a/lemuroid-touchinput/src/main/res/values/dimen.xml +++ b/lemuroid-touchinput/src/main/res/values/dimen.xml @@ -1,5 +1,5 @@ - 192dp - 200dp + 2dp + 2 diff --git a/retrograde-app-shared/build.gradle.kts b/retrograde-app-shared/build.gradle.kts index 1fa81306f1..9ba2295387 100644 --- a/retrograde-app-shared/build.gradle.kts +++ b/retrograde-app-shared/build.gradle.kts @@ -7,6 +7,12 @@ plugins { id("kotlinx-serialization") } +android { + kotlinOptions { + jvmTarget = "1.8" + } +} + dependencies { implementation(project(":retrograde-util")) implementation(project(":lemuroid-touchinput")) diff --git a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/controller/TouchControllerID.kt b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/controller/TouchControllerID.kt index 45b77f5e59..9c24729728 100644 --- a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/controller/TouchControllerID.kt +++ b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/controller/TouchControllerID.kt @@ -1,7 +1,6 @@ package com.swordfish.lemuroid.lib.controller -import com.swordfish.radialgamepad.library.config.RadialGamePadConfig -import com.swordfish.touchinput.radial.RadialPadConfigs +import com.swordfish.touchinput.radial.LemuroidTouchConfigs enum class TouchControllerID { GB, @@ -27,11 +26,12 @@ enum class TouchControllerID { NGP, DOS, WS_LANDSCAPE, - WS_PORTRAIT; + WS_PORTRAIT, + NINTENDO_3DS; class Config( - val leftConfig: RadialGamePadConfig, - val rightConfig: RadialGamePadConfig, + val leftConfig: LemuroidTouchConfigs.Kind, + val rightConfig: LemuroidTouchConfigs.Kind, val leftScale: Float = 1.0f, val rightScale: Float = 1.0f ) @@ -39,30 +39,110 @@ enum class TouchControllerID { companion object { fun getConfig(id: TouchControllerID): Config { return when (id) { - GB -> Config(RadialPadConfigs.GB_LEFT, RadialPadConfigs.GB_RIGHT) - NES -> Config(RadialPadConfigs.NES_LEFT, RadialPadConfigs.NES_RIGHT) - DESMUME -> Config(RadialPadConfigs.DESMUME_LEFT, RadialPadConfigs.DESMUME_RIGHT) - MELONDS -> Config(RadialPadConfigs.MELONDS_NDS_LEFT, RadialPadConfigs.MELONDS_NDS_RIGHT) - PSX -> Config(RadialPadConfigs.PSX_LEFT, RadialPadConfigs.PSX_RIGHT) - PSX_DUALSHOCK -> Config(RadialPadConfigs.PSX_DUALSHOCK_LEFT, RadialPadConfigs.PSX_DUALSHOCK_RIGHT) - N64 -> Config(RadialPadConfigs.N64_LEFT, RadialPadConfigs.N64_RIGHT) - PSP -> Config(RadialPadConfigs.PSP_LEFT, RadialPadConfigs.PSP_RIGHT) - SNES -> Config(RadialPadConfigs.SNES_LEFT, RadialPadConfigs.SNES_RIGHT) - GBA -> Config(RadialPadConfigs.GBA_LEFT, RadialPadConfigs.GBA_RIGHT) - GENESIS_3 -> Config(RadialPadConfigs.GENESIS_3_LEFT, RadialPadConfigs.GENESIS_3_RIGHT) - GENESIS_6 -> Config(RadialPadConfigs.GENESIS_6_LEFT, RadialPadConfigs.GENESIS_6_RIGHT, 1.0f, 1.2f) - ATARI2600 -> Config(RadialPadConfigs.ATARI2600_LEFT, RadialPadConfigs.ATARI2600_RIGHT) - SMS -> Config(RadialPadConfigs.SMS_LEFT, RadialPadConfigs.SMS_RIGHT) - GG -> Config(RadialPadConfigs.GG_LEFT, RadialPadConfigs.GG_RIGHT) - ARCADE_4 -> Config(RadialPadConfigs.ARCADE_4_LEFT, RadialPadConfigs.ARCADE_4_RIGHT) - ARCADE_6 -> Config(RadialPadConfigs.ARCADE_6_LEFT, RadialPadConfigs.ARCADE_6_RIGHT, 1.0f, 1.2f) - LYNX -> Config(RadialPadConfigs.LYNX_LEFT, RadialPadConfigs.LYNX_RIGHT) - ATARI7800 -> Config(RadialPadConfigs.ATARI7800_LEFT, RadialPadConfigs.ATARI7800_RIGHT) - PCE -> Config(RadialPadConfigs.PCE_LEFT, RadialPadConfigs.PCE_RIGHT) - NGP -> Config(RadialPadConfigs.NGP_LEFT, RadialPadConfigs.NGP_RIGHT) - DOS -> Config(RadialPadConfigs.DOS_LEFT, RadialPadConfigs.DOS_RIGHT) - WS_LANDSCAPE -> Config(RadialPadConfigs.WS_LANDSCAPE_LEFT, RadialPadConfigs.WS_LANDSCAPE_RIGHT) - WS_PORTRAIT -> Config(RadialPadConfigs.WS_PORTRAIT_LEFT, RadialPadConfigs.WS_PORTRAIT_RIGHT) + GB -> Config( + LemuroidTouchConfigs.Kind.GB_LEFT, + LemuroidTouchConfigs.Kind.GB_RIGHT + ) + NES -> Config( + LemuroidTouchConfigs.Kind.NES_LEFT, + LemuroidTouchConfigs.Kind.NES_RIGHT + ) + DESMUME -> Config( + LemuroidTouchConfigs.Kind.DESMUME_LEFT, + LemuroidTouchConfigs.Kind.DESMUME_RIGHT + ) + MELONDS -> Config( + LemuroidTouchConfigs.Kind.MELONDS_NDS_LEFT, + LemuroidTouchConfigs.Kind.MELONDS_NDS_RIGHT + ) + PSX -> Config( + LemuroidTouchConfigs.Kind.PSX_LEFT, + LemuroidTouchConfigs.Kind.PSX_RIGHT + ) + PSX_DUALSHOCK -> Config( + LemuroidTouchConfigs.Kind.PSX_DUALSHOCK_LEFT, + LemuroidTouchConfigs.Kind.PSX_DUALSHOCK_RIGHT + ) + N64 -> Config( + LemuroidTouchConfigs.Kind.N64_LEFT, + LemuroidTouchConfigs.Kind.N64_RIGHT + ) + PSP -> Config( + LemuroidTouchConfigs.Kind.PSP_LEFT, + LemuroidTouchConfigs.Kind.PSP_RIGHT + ) + SNES -> Config( + LemuroidTouchConfigs.Kind.SNES_LEFT, + LemuroidTouchConfigs.Kind.SNES_RIGHT + ) + GBA -> Config( + LemuroidTouchConfigs.Kind.GBA_LEFT, + LemuroidTouchConfigs.Kind.GBA_RIGHT + ) + GENESIS_3 -> Config( + LemuroidTouchConfigs.Kind.GENESIS_3_LEFT, + LemuroidTouchConfigs.Kind.GENESIS_3_RIGHT + ) + GENESIS_6 -> Config( + LemuroidTouchConfigs.Kind.GENESIS_6_LEFT, + LemuroidTouchConfigs.Kind.GENESIS_6_RIGHT, + 1.0f, + 1.2f + ) + ATARI2600 -> Config( + LemuroidTouchConfigs.Kind.ATARI2600_LEFT, + LemuroidTouchConfigs.Kind.ATARI2600_RIGHT + ) + SMS -> Config( + LemuroidTouchConfigs.Kind.SMS_LEFT, + LemuroidTouchConfigs.Kind.SMS_RIGHT + ) + GG -> Config( + LemuroidTouchConfigs.Kind.GG_LEFT, + LemuroidTouchConfigs.Kind.GG_RIGHT + ) + ARCADE_4 -> Config( + LemuroidTouchConfigs.Kind.ARCADE_4_LEFT, + LemuroidTouchConfigs.Kind.ARCADE_4_RIGHT + ) + ARCADE_6 -> Config( + LemuroidTouchConfigs.Kind.ARCADE_6_LEFT, + LemuroidTouchConfigs.Kind.ARCADE_6_RIGHT, + 1.0f, + 1.2f + ) + LYNX -> Config( + LemuroidTouchConfigs.Kind.LYNX_LEFT, + LemuroidTouchConfigs.Kind.LYNX_RIGHT + ) + ATARI7800 -> Config( + LemuroidTouchConfigs.Kind.ATARI7800_LEFT, + LemuroidTouchConfigs.Kind.ATARI7800_RIGHT + ) + PCE -> Config( + LemuroidTouchConfigs.Kind.PCE_LEFT, + LemuroidTouchConfigs.Kind.PCE_RIGHT + ) + NGP -> Config( + LemuroidTouchConfigs.Kind.NGP_LEFT, + LemuroidTouchConfigs.Kind.NGP_RIGHT + ) + DOS -> Config( + LemuroidTouchConfigs.Kind.DOS_LEFT, + LemuroidTouchConfigs.Kind.DOS_RIGHT + ) + WS_LANDSCAPE -> Config( + LemuroidTouchConfigs.Kind.WS_LANDSCAPE_LEFT, + LemuroidTouchConfigs.Kind.WS_LANDSCAPE_RIGHT + ) + WS_PORTRAIT -> Config( + LemuroidTouchConfigs.Kind.WS_PORTRAIT_LEFT, + LemuroidTouchConfigs.Kind.WS_PORTRAIT_RIGHT + ) + NINTENDO_3DS -> Config( + LemuroidTouchConfigs.Kind.NINTENDO_3DS_LEFT, + LemuroidTouchConfigs.Kind.NINTENDO_3DS_RIGHT + ) } } } diff --git a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/game/GameLoader.kt b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/game/GameLoader.kt index b00a5efa52..5e15a92b29 100644 --- a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/game/GameLoader.kt +++ b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/game/GameLoader.kt @@ -58,7 +58,8 @@ class GameLoader( appContext: Context, game: Game, loadSave: Boolean, - systemCoreConfig: SystemCoreConfig + systemCoreConfig: SystemCoreConfig, + directLoad: Boolean ): Observable = Observable.create { emitter -> try { emitter.onNext(LoadingState.LoadingCore) @@ -76,7 +77,7 @@ class GameLoader( } val gameFiles = runCatching { - val useVFS = systemCoreConfig.useLibretroVFS + val useVFS = systemCoreConfig.supportsLibretroVFS && directLoad val dataFiles = retrogradeDatabase.dataFileDao().selectDataFilesForGame(game.id) lemuroidLibrary.getGameFiles(game, dataFiles, useVFS).blockingGet() }.getOrElse { throw GameLoaderException(GameLoaderError.LOAD_GAME) } diff --git a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/ControllerConfigs.kt b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/ControllerConfigs.kt index 2ebcab6bdd..7c1cf8dc22 100644 --- a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/ControllerConfigs.kt +++ b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/ControllerConfigs.kt @@ -209,4 +209,11 @@ object ControllerConfigs { TouchControllerID.WS_PORTRAIT, mergeDPADAndLeftStickEvents = true, ) + + val NINTENDO_3DS = ControllerConfig( + "default", + R.string.controller_default, + TouchControllerID.NINTENDO_3DS, + allowTouchOverlay = false + ) } diff --git a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/CoreID.kt b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/CoreID.kt index 07b1908369..2a65221bd3 100644 --- a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/CoreID.kt +++ b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/CoreID.kt @@ -102,6 +102,11 @@ enum class CoreID( "Beetle Cygne", "libmednafen_wswan_libretro_android.so" ), + CITRA( + "citra", + "Citra", + "libcitra_libretro_android.so" + ), DOSBOX_PURE( "dosbox_pure", "DosBox Pure", diff --git a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/GameSystem.kt b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/GameSystem.kt index f23b816977..06865a21f1 100644 --- a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/GameSystem.kt +++ b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/GameSystem.kt @@ -528,6 +528,7 @@ data class GameSystem( R.string.setting_gambatte_dark_filter_level ) ), + rumbleSupported = true, defaultSettings = listOf( CoreVariable("gambatte_gbc_color_correction", "disabled"), ), @@ -599,6 +600,7 @@ data class GameSystem( ) ), ), + rumbleSupported = true, controllerConfigs = hashMapOf( 0 to arrayListOf(ControllerConfigs.GBA) ) @@ -651,13 +653,46 @@ data class GameSystem( ), ) ), + ExposedSetting( + "mupen64plus-pak1", + R.string.setting_mupen64plus_pak1, + arrayListOf( + ExposedSetting.Value( + "memory", + R.string.value_mupen64plus_mupen64plus_pak1_memory + ), + ExposedSetting.Value( + "rumble", + R.string.value_mupen64plus_mupen64plus_pak1_rumble + ), + ExposedSetting.Value( + "none", + R.string.value_mupen64plus_mupen64plus_pak1_none + ) + ) + ), + ExposedSetting( + "mupen64plus-pak2", + R.string.setting_mupen64plus_pak2, + arrayListOf( + ExposedSetting.Value( + "none", + R.string.value_mupen64plus_mupen64plus_pak2_none + ), + ExposedSetting.Value( + "rumble", + R.string.value_mupen64plus_mupen64plus_pak2_rumble + ) + ) + ) ), defaultSettings = listOf( CoreVariable("mupen64plus-43screensize", "320x240") ), controllerConfigs = hashMapOf( 0 to arrayListOf(ControllerConfigs.N64) - ) + ), + rumbleSupported = true ) ), uniqueExtensions = listOf("n64", "z64"), @@ -702,8 +737,11 @@ data class GameSystem( ), defaultSettings = listOf( CoreVariable("pcsx_rearmed_drc", "disabled"), + CoreVariable("pcsx_rearmed_duping_enable", "enabled"), ), - useLibretroVFS = true + rumbleSupported = true, + supportsLibretroVFS = true, + skipDuplicateFrames = false ) ), uniqueExtensions = listOf(), @@ -738,13 +776,13 @@ data class GameSystem( "ppsspp_cpu_core", R.string.setting_ppsspp_cpu_core, arrayListOf( - ExposedSetting.Value("jit", R.string.value_ppsspp_cpu_core_jit), + ExposedSetting.Value("JIT", R.string.value_ppsspp_cpu_core_jit), ExposedSetting.Value( - "IR jit", + "IR JIT", R.string.value_ppsspp_cpu_core_irjit ), ExposedSetting.Value( - "interpreter", + "Interpreter", R.string.value_ppsspp_cpu_core_interpreter ), ) @@ -760,7 +798,8 @@ data class GameSystem( ), controllerConfigs = hashMapOf( 0 to arrayListOf(ControllerConfigs.PSP) - ) + ), + supportsLibretroVFS = true ) ), uniqueExtensions = listOf(), @@ -1099,6 +1138,30 @@ data class GameSystem( scanByPathAndSupportedExtensions = true ), ), + GameSystem( + SystemID.NINTENDO_3DS, + "Nintendo - Nintendo 3DS", + R.string.game_system_title_3ds, + R.string.game_system_abbr_3ds, + listOf( + SystemCoreConfig( + CoreID.CITRA, + controllerConfigs = hashMapOf( + 0 to arrayListOf(ControllerConfigs.NINTENDO_3DS) + ), + defaultSettings = listOf( + CoreVariable("citra_use_acc_mul", "disabled"), + CoreVariable("citra_touch_touchscreen", "enabled"), + CoreVariable("citra_mouse_touchscreen", "disabled"), + CoreVariable("citra_render_touchscreen", "disabled"), + CoreVariable("citra_use_hw_shader_cache", "disabled"), + ), + statesSupported = false, + supportsLibretroVFS = true + ), + ), + uniqueExtensions = listOf("3ds"), + ), ) private val byIdCache by lazy { mapOf(*SYSTEMS.map { it.id.dbname to it }.toTypedArray()) } diff --git a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/LemuroidLibrary.kt b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/LemuroidLibrary.kt index ee9dc20deb..7788f3611e 100644 --- a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/LemuroidLibrary.kt +++ b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/LemuroidLibrary.kt @@ -77,9 +77,9 @@ class LemuroidLibrary( handleUnknownFiles(provider, unknownFiles, startedAtMs) } } - .doOnComplete { removeDeletedBios(startedAtMs) } - .doOnComplete { removeDeletedGames(startedAtMs) } - .doOnComplete { removeDeletedDataFiles(startedAtMs) } + .doFinally { removeDeletedBios(startedAtMs) } + .doFinally { removeDeletedGames(startedAtMs) } + .doFinally { removeDeletedDataFiles(startedAtMs) } .doOnComplete { Timber.i( "Library indexing completed in: ${System.currentTimeMillis() - startedAtMs} ms" diff --git a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/MetaSystemID.kt b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/MetaSystemID.kt index fd22c3c4a2..a47d076700 100644 --- a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/MetaSystemID.kt +++ b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/MetaSystemID.kt @@ -106,6 +106,11 @@ enum class MetaSystemID(val titleResId: Int, val imageResId: Int, val systemIDs: R.string.game_system_title_dos, R.drawable.game_system_dos, listOf(SystemID.DOS) + ), + NINTENDO_3DS( + R.string.game_system_title_3ds, + R.drawable.game_system_3ds, + listOf(SystemID.NINTENDO_3DS) ); companion object { @@ -135,6 +140,7 @@ enum class MetaSystemID(val titleResId: Int, val imageResId: Int, val systemIDs: SystemID.NGC -> NGP SystemID.WS -> WS SystemID.WSC -> WS + SystemID.NINTENDO_3DS -> NINTENDO_3DS } } } diff --git a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/SystemCoreConfig.kt b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/SystemCoreConfig.kt index 6abada0789..99d5953800 100644 --- a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/SystemCoreConfig.kt +++ b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/SystemCoreConfig.kt @@ -11,7 +11,9 @@ data class SystemCoreConfig( val exposedAdvancedSettings: List = listOf(), val defaultSettings: List = listOf(), val statesSupported: Boolean = true, + val rumbleSupported: Boolean = false, val requiredBIOSFiles: List = listOf(), val statesVersion: Int = 0, - val useLibretroVFS: Boolean = false + val supportsLibretroVFS: Boolean = false, + val skipDuplicateFrames: Boolean = true ) : Serializable diff --git a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/SystemID.kt b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/SystemID.kt index a8e9e08700..ce326932d8 100644 --- a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/SystemID.kt +++ b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/library/SystemID.kt @@ -25,4 +25,5 @@ enum class SystemID(val dbname: String) { WS("ws"), WSC("wsc"), DOS("dos"), + NINTENDO_3DS("3ds"), } diff --git a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/storage/cache/CacheCleaner.kt b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/storage/cache/CacheCleaner.kt index 4656b79965..d520d636b8 100644 --- a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/storage/cache/CacheCleaner.kt +++ b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/storage/cache/CacheCleaner.kt @@ -5,17 +5,35 @@ import android.os.Environment import android.os.StatFs import android.system.Os import android.text.format.Formatter +import com.swordfish.lemuroid.common.kotlin.gigaBytes +import com.swordfish.lemuroid.common.kotlin.megaBytes import com.swordfish.lemuroid.lib.storage.local.LocalStorageProvider import com.swordfish.lemuroid.lib.storage.local.StorageAccessFrameworkProvider import io.reactivex.Completable import timber.log.Timber import java.io.File +import kotlin.math.abs +import kotlin.math.roundToLong object CacheCleaner { - fun getOptimalCacheSize(): Long { - // We are capping cache size to be 1/20th of total internal memory... - return getInternalMemorySize() / 20 + private val MIN_CACHE_LIMIT = 64L.megaBytes() + private val MAX_CACHE_LIMIT = 10L.gigaBytes() + + fun getSupportedCacheLimits(): List { + return generateSequence(MIN_CACHE_LIMIT) { it * 2L } + .takeWhile { it <= MAX_CACHE_LIMIT } + .toList() + } + + fun getDefaultCacheLimit(): Long { + val defaultCacheSize = (getInternalMemorySize() * 0.01f).roundToLong() + return getClosestCacheLimit(defaultCacheSize) + } + + private fun getClosestCacheLimit(size: Long): Long { + return getSupportedCacheLimits() + .minByOrNull { abs(it - size) } ?: 0 } private fun getInternalMemorySize(): Long { @@ -29,8 +47,9 @@ object CacheCleaner { appContext.cacheDir.listFiles()?.forEach { it.deleteRecursively() } } - fun clean(appContext: Context, maxByteSize: Long) = Completable.fromAction { + fun clean(appContext: Context, requestedLimit: Long) = Completable.fromAction { Timber.i("Running cache cleanup lru task") + val cacheLimit = getClosestCacheLimit(requestedLimit) val cacheFoldersSequence = sequenceOf( File(appContext.cacheDir, StorageAccessFrameworkProvider.SAF_CACHE_SUBFOLDER).walkBottomUp(), @@ -46,9 +65,9 @@ object CacheCleaner { .map { it.length() } .sum() - Timber.i("Space used by cache: ${printSize(appContext, cacheSize)} / ${printSize(appContext, maxByteSize)}") + Timber.i("Space used by cache: ${printSize(appContext, cacheSize)} / ${printSize(appContext, cacheLimit)}") - var spaceToBeDeleted = maxOf(cacheSize - maxByteSize, 0) + var spaceToBeDeleted = maxOf(cacheSize - cacheLimit, 0) Timber.i("Freeing cache space: ${printSize(appContext, spaceToBeDeleted)}") diff --git a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/storage/local/DocumentFileParser.kt b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/storage/local/DocumentFileParser.kt index dcb8ce19a2..ef3f4de7e2 100644 --- a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/storage/local/DocumentFileParser.kt +++ b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/storage/local/DocumentFileParser.kt @@ -4,7 +4,7 @@ import android.content.Context import com.swordfish.lemuroid.common.kotlin.calculateCrc32 import com.swordfish.lemuroid.common.kotlin.toStringCRC32 import com.swordfish.lemuroid.lib.storage.BaseStorageFile -import com.swordfish.lemuroid.lib.storage.scanner.DiskScanner +import com.swordfish.lemuroid.lib.storage.scanner.SerialScanner import com.swordfish.lemuroid.lib.storage.StorageFile import timber.log.Timber import java.util.zip.ZipEntry @@ -47,7 +47,7 @@ object DocumentFileParser { ): StorageFile { Timber.d("Processing zipped entry: ${entry.name}") - val diskInfo = DiskScanner.extractInfo(entry.name, zipInputStream) + val diskInfo = SerialScanner.extractInfo(entry.name, zipInputStream) return StorageFile( entry.name, @@ -62,7 +62,7 @@ object DocumentFileParser { private fun parseStandardFile(context: Context, baseStorageFile: BaseStorageFile): StorageFile { val diskInfo = context.contentResolver.openInputStream(baseStorageFile.uri) - ?.let { inputStream -> DiskScanner.extractInfo(baseStorageFile.name, inputStream) } + ?.let { inputStream -> SerialScanner.extractInfo(baseStorageFile.name, inputStream) } val crc32 = if (baseStorageFile.size < MAX_SIZE_CRC32 && diskInfo?.serial == null) { context.contentResolver.openInputStream(baseStorageFile.uri)?.calculateCrc32() diff --git a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/storage/scanner/DiskScanner.kt b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/storage/scanner/SerialScanner.kt similarity index 92% rename from retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/storage/scanner/DiskScanner.kt rename to retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/storage/scanner/SerialScanner.kt index 6bb8ae01fe..60b89fa6b4 100644 --- a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/storage/scanner/DiskScanner.kt +++ b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/storage/scanner/SerialScanner.kt @@ -2,8 +2,8 @@ package com.swordfish.lemuroid.lib.storage.scanner import com.swordfish.lemuroid.common.files.FileUtils import com.swordfish.lemuroid.common.kotlin.indexOf -import com.swordfish.lemuroid.common.kotlin.kb -import com.swordfish.lemuroid.common.kotlin.mb +import com.swordfish.lemuroid.common.kotlin.kiloBytes +import com.swordfish.lemuroid.common.kotlin.megaBytes import com.swordfish.lemuroid.common.kotlin.startsWithAny import com.swordfish.lemuroid.lib.library.SystemID import timber.log.Timber @@ -12,8 +12,8 @@ import java.nio.charset.Charset import kotlin.math.ceil import kotlin.math.roundToInt -object DiskScanner { - private val READ_BUFFER_SIZE = 64.kb() +object SerialScanner { + private val READ_BUFFER_SIZE = 64.kiloBytes() data class DiskInfo(val serial: String?, val systemID: SystemID?) @@ -144,6 +144,7 @@ object DiskScanner { return when (FileUtils.extractExtension(fileName)) { "pbp" -> extractInfoForPBP(it) "iso", "bin" -> standardExtractInfo(it) + "3ds" -> extractInfoFor3DS(it) else -> DiskInfo(null, null) } } @@ -180,6 +181,19 @@ object DiskScanner { } } + private fun extractInfoFor3DS(openedStream: InputStream): DiskInfo { + Timber.d("Parsing 3DS game") + openedStream.mark(0x2000) + openedStream.skip(0x1150) + + val rawSerial = String(readByteArray(openedStream, ByteArray(10)), Charsets.US_ASCII) + + openedStream.reset() + + Timber.d("Found 3DS serial: $rawSerial") + return DiskInfo(rawSerial, SystemID.NINTENDO_3DS) + } + private fun extractInfoForSegaCD(openedStream: InputStream): DiskInfo { Timber.d("Parsing SegaCD game") openedStream.mark(20000) @@ -220,7 +234,7 @@ object DiskScanner { } private fun extractInfoForPSX(openedStream: InputStream): DiskInfo { - val headerSize = 64.kb() + val headerSize = 64.kiloBytes() if (openedStream.available() < headerSize) { return DiskInfo(null, null) } @@ -232,7 +246,7 @@ object DiskScanner { } private fun extractInfoForPSP(openedStream: InputStream): DiskInfo { - val headerSize = 64.kb() + val headerSize = 64.kiloBytes() if (openedStream.available() < headerSize) { return DiskInfo(null, null) } @@ -244,7 +258,7 @@ object DiskScanner { } private fun extractInfoForPBP(openedStream: InputStream): DiskInfo { - val headerSize = 2.mb() + val headerSize = 2.megaBytes() if (openedStream.available() < headerSize) { return DiskInfo(null, null) } @@ -276,7 +290,7 @@ object DiskScanner { openedStream: InputStream, resultSize: Int, streamSize: Int, - windowSize: Int = 8.kb(), + windowSize: Int = 8.kiloBytes(), skipSize: Int = windowSize - resultSize, charset: Charset = Charsets.US_ASCII ): Sequence { diff --git a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/ui/ViewUtils.kt b/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/ui/ViewUtils.kt deleted file mode 100644 index 029e256e26..0000000000 --- a/retrograde-app-shared/src/main/java/com/swordfish/lemuroid/lib/ui/ViewUtils.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.swordfish.lemuroid.lib.ui - -import android.view.View - -fun View.setVisibleOrGone(visible: Boolean) { - this.visibility = if (visible) View.VISIBLE else View.GONE -} - -fun View.setVisibleOrInvisible(visible: Boolean) { - this.visibility = if (visible) View.VISIBLE else View.INVISIBLE -} diff --git a/retrograde-app-shared/src/main/res/drawable/game_system_3ds.xml b/retrograde-app-shared/src/main/res/drawable/game_system_3ds.xml new file mode 100644 index 0000000000..dd419b2c5c --- /dev/null +++ b/retrograde-app-shared/src/main/res/drawable/game_system_3ds.xml @@ -0,0 +1,10 @@ + + + + diff --git a/retrograde-app-shared/src/main/res/values/colors.xml b/retrograde-app-shared/src/main/res/values/colors.xml index 3f907e7554..383beeb7eb 100644 --- a/retrograde-app-shared/src/main/res/values/colors.xml +++ b/retrograde-app-shared/src/main/res/values/colors.xml @@ -1,18 +1,10 @@ - - @color/main_color_light - @color/main_color + + - @color/colorPrimary - @color/colorPrimaryVariant + + - #080808 - #222222 - #181818 - - #09ffffff - - #99000000 diff --git a/retrograde-app-shared/src/main/res/values/strings-game-system.xml b/retrograde-app-shared/src/main/res/values/strings-game-system.xml index 423f18afdf..c720b76186 100644 --- a/retrograde-app-shared/src/main/res/values/strings-game-system.xml +++ b/retrograde-app-shared/src/main/res/values/strings-game-system.xml @@ -43,6 +43,7 @@ WS WSC DOS + 3DS Nintendo Atari 2600 @@ -68,6 +69,7 @@ WonderSwan WonderSwan Color DOS (Beta) + Nintendo 3DS (Beta) Arcade diff --git a/retrograde-app-shared/src/main/res/values/strings.xml b/retrograde-app-shared/src/main/res/values/strings.xml index 9852ce5102..44627a8d2b 100644 --- a/retrograde-app-shared/src/main/res/values/strings.xml +++ b/retrograde-app-shared/src/main/res/values/strings.xml @@ -40,6 +40,8 @@ Screen size 4:3 Dynamic recompiler Bilinear mode + Player 1 Pak + Player 2 Pak Frameskip Dynamic recompiler Auto frameskip @@ -102,6 +104,13 @@ Standard 3-Point + Memory + Rumble + None + + None + Rumble + Top - Bottom Left - Right Hybrid Top diff --git a/retrograde-app-shared/src/main/res/values/styles.xml b/retrograde-app-shared/src/main/res/values/styles.xml index 5d706d4a6e..57fbb4053d 100644 --- a/retrograde-app-shared/src/main/res/values/styles.xml +++ b/retrograde-app-shared/src/main/res/values/styles.xml @@ -1,5 +1,4 @@ - - - - - - - - - - - - - - - - - diff --git a/retrograde-app-shared/src/main/res/values/styles_leanback.xml b/retrograde-app-shared/src/main/res/values/styles_leanback.xml index a85565f324..d781ec5f1e 100644 --- a/retrograde-app-shared/src/main/res/values/styles_leanback.xml +++ b/retrograde-app-shared/src/main/res/values/styles_leanback.xml @@ -1,30 +1,4 @@ - - @color/leanbackForeground - @color/leanbackInfo - - - - - - diff --git a/retrograde-util/build.gradle.kts b/retrograde-util/build.gradle.kts index 527e682891..8578cf785b 100644 --- a/retrograde-util/build.gradle.kts +++ b/retrograde-util/build.gradle.kts @@ -1,6 +1,13 @@ plugins { id("com.android.library") id("kotlin-android") + id("kotlin-kapt") +} + +android { + kotlinOptions { + jvmTarget = "1.8" + } } dependencies { diff --git a/retrograde-util/src/main/java/com/swordfish/lemuroid/common/kotlin/IntKt.kt b/retrograde-util/src/main/java/com/swordfish/lemuroid/common/kotlin/IntKt.kt index bc0e1443ec..28e189c537 100644 --- a/retrograde-util/src/main/java/com/swordfish/lemuroid/common/kotlin/IntKt.kt +++ b/retrograde-util/src/main/java/com/swordfish/lemuroid/common/kotlin/IntKt.kt @@ -1,5 +1,5 @@ package com.swordfish.lemuroid.common.kotlin -fun Int.kb(): Int = this * 1024 +fun Int.kiloBytes(): Int = this * 1000 -fun Int.mb(): Int = this * 1024 * 1024 +fun Int.megaBytes(): Int = this.kiloBytes() * 1000 diff --git a/retrograde-util/src/main/java/com/swordfish/lemuroid/common/kotlin/LongKt.kt b/retrograde-util/src/main/java/com/swordfish/lemuroid/common/kotlin/LongKt.kt new file mode 100644 index 0000000000..66607c8714 --- /dev/null +++ b/retrograde-util/src/main/java/com/swordfish/lemuroid/common/kotlin/LongKt.kt @@ -0,0 +1,7 @@ +package com.swordfish.lemuroid.common.kotlin + +fun Long.kiloBytes(): Long = this * 1000L + +fun Long.megaBytes(): Long = this.kiloBytes() * 1000L + +fun Long.gigaBytes(): Long = this.megaBytes() * 1000L diff --git a/retrograde-util/src/main/java/com/swordfish/lemuroid/common/kotlin/Utils.kt b/retrograde-util/src/main/java/com/swordfish/lemuroid/common/kotlin/Utils.kt index e19b490354..460b375a5a 100644 --- a/retrograde-util/src/main/java/com/swordfish/lemuroid/common/kotlin/Utils.kt +++ b/retrograde-util/src/main/java/com/swordfish/lemuroid/common/kotlin/Utils.kt @@ -1,6 +1,7 @@ package com.swordfish.lemuroid.common.kotlin data class NTuple4(val t1: T1, val t2: T2, val t3: T3, val t4: T4) +data class NTuple5(val t1: T1, val t2: T2, val t3: T3, val t4: T4, val t5: T5) fun Long.toStringCRC32(): String { return "%08x".format(this).toUpperCase() diff --git a/retrograde-util/src/main/java/com/swordfish/lemuroid/common/math/MathUtils.kt b/retrograde-util/src/main/java/com/swordfish/lemuroid/common/math/MathUtils.kt index 68b13b91ff..ef7ab872d6 100644 --- a/retrograde-util/src/main/java/com/swordfish/lemuroid/common/math/MathUtils.kt +++ b/retrograde-util/src/main/java/com/swordfish/lemuroid/common/math/MathUtils.kt @@ -1,7 +1,6 @@ package com.swordfish.lemuroid.common.math import android.graphics.PointF -import kotlin.math.atan2 import kotlin.math.cos import kotlin.math.sin import kotlin.math.sqrt @@ -9,15 +8,6 @@ import kotlin.math.sqrt fun linearInterpolation(t: Float, a: Float, b: Float) = (a * (1.0f - t)) + (b * t) object MathUtils { - /** Compute the angle with the x axis of the line between two points. Results in range [0,2pi[.*/ - fun angle(x1: Float, x2: Float, y1: Float, y2: Float): Float { - return ((-atan2(y2 - y1, x2 - x1) + PI2) % (PI2)) - } - - fun clamp(x: Float, min: Float, max: Float): Float { - return maxOf(minOf(x, max), min) - } - fun distance(x1: Float, x2: Float, y1: Float, y2: Float): Float { return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)) } diff --git a/retrograde-util/src/main/java/com/swordfish/lemuroid/common/rx/RXUtils.kt b/retrograde-util/src/main/java/com/swordfish/lemuroid/common/rx/RXUtils.kt index 5ae3b98298..187e44db77 100644 --- a/retrograde-util/src/main/java/com/swordfish/lemuroid/common/rx/RXUtils.kt +++ b/retrograde-util/src/main/java/com/swordfish/lemuroid/common/rx/RXUtils.kt @@ -1,6 +1,7 @@ package com.swordfish.lemuroid.common.rx import com.swordfish.lemuroid.common.kotlin.NTuple4 +import com.swordfish.lemuroid.common.kotlin.NTuple5 import io.reactivex.Observable object RXUtils { @@ -10,12 +11,27 @@ object RXUtils { source3: Observable, source4: Observable ): Observable> { + return Observable.combineLatest( + source1, + source2, + source3, + source4 + ) { t1, t2, t3, t4 -> NTuple4(t1, t2, t3, t4) } + } + + fun combineLatest( + source1: Observable, + source2: Observable, + source3: Observable, + source4: Observable, + source5: Observable + ): Observable> { return Observable.combineLatest( source1, source2, source3, source4, - { t1, t2, t3, t4 -> NTuple4(t1, t2, t3, t4) } - ) + source5 + ) { t1, t2, t3, t4, t5 -> NTuple5(t1, t2, t3, t4, t5) } } } diff --git a/retrograde-util/src/main/java/com/swordfish/lemuroid/common/view/ViewUtils.kt b/retrograde-util/src/main/java/com/swordfish/lemuroid/common/view/ViewUtils.kt new file mode 100644 index 0000000000..164c72014d --- /dev/null +++ b/retrograde-util/src/main/java/com/swordfish/lemuroid/common/view/ViewUtils.kt @@ -0,0 +1,39 @@ +package com.swordfish.lemuroid.common.view + +import android.animation.ObjectAnimator +import android.view.View +import android.widget.ProgressBar +import androidx.core.animation.addListener + +fun View.setVisibleOrGone(visible: Boolean) { + this.visibility = if (visible) View.VISIBLE else View.GONE +} + +fun View.setVisibleOrInvisible(visible: Boolean) { + this.visibility = if (visible) View.VISIBLE else View.INVISIBLE +} + +fun View.animateVisibleOrGone(visible: Boolean, durationInMs: Long) { + val alpha = if (visible) 1.0f else 0.0f + ObjectAnimator.ofFloat(this, "alpha", alpha).apply { + duration = durationInMs + setAutoCancel(true) + addListener( + onStart = { + if (visible) setVisibleOrGone(true) + }, + onEnd = { + if (!visible) setVisibleOrGone(false) + } + ) + start() + } +} + +fun ProgressBar.animateProgress(progress: Int, durationInMs: Long) { + ObjectAnimator.ofInt(this, "progress", progress).apply { + duration = durationInMs + setAutoCancel(true) + start() + } +} diff --git a/retrograde-util/src/main/res/values/colors.xml b/retrograde-util/src/main/res/values/colors.xml new file mode 100644 index 0000000000..4a584d9ded --- /dev/null +++ b/retrograde-util/src/main/res/values/colors.xml @@ -0,0 +1,13 @@ + + + #121212 + #1D1D1D + #212121 + #242424 + #272727 + #2C2C2C + #2D2D2D + #323232 + #353535 + #383838 + diff --git a/settings.gradle.kts b/settings.gradle.kts index 42e4ea72da..693cd10306 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,7 +36,8 @@ if (usePlayDynamicFeatures()) { ":lemuroid_core_ppsspp", ":lemuroid_core_prosystem", ":lemuroid_core_snes9x", - ":lemuroid_core_stella" + ":lemuroid_core_stella", + ":lemuroid_core_citra" ) project(":lemuroid_core_gambatte").projectDir = File("lemuroid-cores/lemuroid_core_gambatte") @@ -58,4 +59,5 @@ if (usePlayDynamicFeatures()) { project(":lemuroid_core_mednafen_ngp").projectDir = File("lemuroid-cores/lemuroid_core_mednafen_ngp") project(":lemuroid_core_mednafen_wswan").projectDir = File("lemuroid-cores/lemuroid_core_mednafen_wswan") project(":lemuroid_core_dosbox_pure").projectDir = File("lemuroid-cores/lemuroid_core_dosbox_pure") + project(":lemuroid_core_citra").projectDir = File("lemuroid-cores/lemuroid_core_citra") }