├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ └── config.yml
└── workflows
│ └── build.yml
├── .gitignore
├── .gitmodules
├── .idea
├── .gitignore
├── .name
├── compiler.xml
├── deploymentTargetSelector.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── kotlinc.xml
├── migrations.xml
├── misc.xml
├── render.experimental.xml
├── runConfigurations.xml
└── vcs.xml
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── net
│ │ └── rpcs3
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── cpp
│ │ ├── CMakeLists.txt
│ │ ├── block_dev.hpp
│ │ ├── compile_commands.json
│ │ ├── fs_provider.hpp
│ │ ├── iso.cpp
│ │ ├── iso.hpp
│ │ └── native-lib.cpp
│ ├── ic_rpcs3-playstore.png
│ ├── java
│ │ └── net
│ │ │ └── rpcs3
│ │ │ ├── FirmwareRepository.kt
│ │ │ ├── GameRepository.kt
│ │ │ ├── GraphicsFrame.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── Permission.kt
│ │ │ ├── PrecompilerService.kt
│ │ │ ├── ProgressRepository.kt
│ │ │ ├── RPCS3.kt
│ │ │ ├── RPCS3Activity.kt
│ │ │ ├── RPCS3Theme.kt
│ │ │ ├── UsbDeviceRepository.kt
│ │ │ ├── dialogs
│ │ │ └── AlertDialogQueue.kt
│ │ │ ├── overlay
│ │ │ ├── OverlayEditActivity.kt
│ │ │ ├── PadOverlay.kt
│ │ │ ├── PadOverlayButton.kt
│ │ │ ├── PadOverlayDpad.kt
│ │ │ └── PadOverlayStick.kt
│ │ │ ├── provider
│ │ │ └── AppDataDocumentProvider.kt
│ │ │ ├── ui
│ │ │ ├── common
│ │ │ │ └── Previews.kt
│ │ │ ├── drivers
│ │ │ │ └── GpuDriversScreen.kt
│ │ │ ├── games
│ │ │ │ └── GamesScreen.kt
│ │ │ ├── navigation
│ │ │ │ └── AppNavHost.kt
│ │ │ └── settings
│ │ │ │ ├── SettingsScreen.kt
│ │ │ │ ├── components
│ │ │ │ ├── CompositionLocals.kt
│ │ │ │ ├── base
│ │ │ │ │ ├── BaseDialogPreference.kt
│ │ │ │ │ └── BasePreference.kt
│ │ │ │ ├── core
│ │ │ │ │ ├── MaterialSwitch.kt
│ │ │ │ │ ├── PreferenceIcon.kt
│ │ │ │ │ └── PreferenceTitle.kt
│ │ │ │ └── preference
│ │ │ │ │ ├── CheckboxPreference.kt
│ │ │ │ │ ├── HomePreference.kt
│ │ │ │ │ ├── ListPreference.kt
│ │ │ │ │ ├── RegularPreference.kt
│ │ │ │ │ ├── SliderPreference.kt
│ │ │ │ │ └── SwitchPreference.kt
│ │ │ │ └── util
│ │ │ │ ├── ModifierExt.kt
│ │ │ │ └── Theming.kt
│ │ │ └── utils
│ │ │ ├── DriverPackageMetadata.kt
│ │ │ ├── DriversFetcher.kt
│ │ │ ├── FileUtil.kt
│ │ │ ├── GpuDriverHelper.kt
│ │ │ └── ZipUtil.kt
│ └── res
│ │ ├── drawable
│ │ ├── circle.png
│ │ ├── cross.png
│ │ ├── dpad_bottom.png
│ │ ├── dpad_left.png
│ │ ├── dpad_right.png
│ │ ├── dpad_top.png
│ │ ├── ic_description.xml
│ │ ├── ic_folder.xml
│ │ ├── ic_grid_off.xml
│ │ ├── ic_grid_on.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_launcher_foreground.xml
│ │ ├── ic_osc_off.xml
│ │ ├── ic_palette.xml
│ │ ├── ic_play.xml
│ │ ├── ic_restore.xml
│ │ ├── ic_rpcs3_foreground.xml
│ │ ├── ic_rpcs3_monochrome.xml
│ │ ├── ic_show_osc.xml
│ │ ├── ic_stop.xml
│ │ ├── l1.png
│ │ ├── l2.png
│ │ ├── l3.png
│ │ ├── left_stick.png
│ │ ├── left_stick_background.png
│ │ ├── r1.png
│ │ ├── r2.png
│ │ ├── r3.png
│ │ ├── right_stick.png
│ │ ├── right_stick_background.png
│ │ ├── rounded_background.xml
│ │ ├── select.png
│ │ ├── square.png
│ │ ├── start.png
│ │ └── triangle.png
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ └── activity_rpcs3.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_rpcs3.xml
│ │ └── ic_rpcs3_round.xml
│ │ ├── mipmap-anydpi
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_round.webp
│ │ ├── ic_rpcs3.webp
│ │ └── ic_rpcs3_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_round.webp
│ │ ├── ic_rpcs3.webp
│ │ └── ic_rpcs3_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_round.webp
│ │ ├── ic_rpcs3.webp
│ │ └── ic_rpcs3_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_round.webp
│ │ ├── ic_rpcs3.webp
│ │ └── ic_rpcs3_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_round.webp
│ │ ├── ic_rpcs3.webp
│ │ └── ic_rpcs3_round.webp
│ │ ├── values-night
│ │ └── themes.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── ic_rpcs3_background.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ ├── data_extraction_rules.xml
│ │ └── game_config.xml
│ └── test
│ └── java
│ └── net
│ └── rpcs3
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug report
2 | description: Create a report to help us improve and investigate the issue
3 | title: "[Bug] "
4 | labels: bug
5 | body:
6 | - type: input
7 | id: app-title
8 | attributes:
9 | label: Describe the bug
10 | description: A clear and concise description of what the bug is.
11 | validations:
12 | required: true
13 |
14 | - type: textarea
15 | id: stepstoreproducebug
16 | attributes:
17 | label: Steps to reproduce the behavior
18 | value: |
19 | - 1. Go to '...'
20 | - 2. Click on '....'
21 | - 3. Scroll down to '....'
22 | - 4. See error
23 | validations:
24 | required: true
25 |
26 | - type: textarea
27 | id: upload-screenshots
28 | attributes:
29 | label: Screenshots
30 | description: If applicable, add screenshots to help explain your problem.
31 | placeholder: Upload your screenshots here
32 | validations:
33 | required: false
34 |
35 | - type: textarea
36 | id: phonespecs
37 | attributes:
38 | label: Smartphone (please complete the following information)
39 | value: |
40 | - Device: [e.g. Pixel 32 Pro]
41 | - OS: [e.g. OneUI 21]
42 | - Version [e.g. v2.1.0]
43 | validations:
44 | required: true
45 |
46 | - type: textarea
47 | id: extracontext
48 | attributes:
49 | label: Additional context
50 | description: Add any other context about the problem here.
51 | validations:
52 | required: false
53 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: RPCS3-Android Discord Server
4 | url: https://discord.gg/3PprUAVjfr
5 | about: The Official RPCS3-Android Discord Server
6 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | # Todo List:
2 | # - Move librpcs3 compilation away (or add ccache here) because the compilation is ridiculously slow
3 | # - Do not use legacy CMake scripts (figure out how)
4 |
5 | name: Java CI with Gradle
6 |
7 | on:
8 | workflow_dispatch:
9 | inputs:
10 | shouldrelease:
11 | description: Create release
12 | type: boolean
13 | uploadbuild:
14 | description: Upload build
15 | type: boolean
16 | releasetag:
17 | description: Enter the tag name
18 | type: string
19 | releasetype:
20 | description: Make it prerelease
21 | type: boolean
22 | islatest:
23 | description: Mark as latest
24 | type: boolean
25 | push:
26 | branches: [ "master" ]
27 | pull_request:
28 | branches: [ "master" ]
29 |
30 | jobs:
31 | build:
32 | runs-on: ubuntu-latest
33 | permissions:
34 | contents: write
35 |
36 | steps:
37 | - name: Checkout repo
38 | uses: actions/checkout@v4
39 | with:
40 | submodules: recursive
41 |
42 | - name: Setup CCache
43 | uses: hendrikmuhs/ccache-action@v1.2
44 |
45 | - name: Setup Gradle Cache
46 | uses: actions/cache@v4
47 | with:
48 | path: |
49 | ~/.gradle/caches
50 | ~/.gradle/wrapper
51 | key: ${{ runner.os }}-android-${{ github.sha }}
52 | restore-keys: |
53 | ${{ runner.os }}-android-
54 |
55 | - name: Setup Java
56 | uses: actions/setup-java@v4
57 | with:
58 | distribution: 'temurin'
59 | java-version: 17
60 |
61 | - name: Decode Keystore
62 | env:
63 | KEYSTORE_ENCODED: ${{ secrets.KEYSTORE }}
64 | run: |
65 | echo "$KEYSTORE_ENCODED" | base64 --decode > ${{ github.workspace }}/app/ks.jks
66 |
67 | - name: Build with Gradle
68 | env:
69 | KEYSTORE_PATH: "ks.jks"
70 | KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
71 | KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
72 | CMAKE_C_COMPILER_LAUNCHER: "ccache"
73 | CMAKE_CXX_COMPILER_LAUNCHER: "ccache"
74 | run: ./gradlew assembleRelease
75 |
76 | - name: Build release artifacts
77 | uses: actions/upload-artifact@v4
78 | if: ${{ github.event_name != 'workflow_dispatch' }} || ${{ inputs.uploadbuild }}
79 | with:
80 | name: rpcs3-build
81 | path: |
82 | app/build/outputs/apk/release/rpcs3-release.apk
83 |
84 | - name: Create release
85 | uses: softprops/action-gh-release@v2
86 | if: ${{ inputs.shouldrelease }}
87 | with:
88 | prerelease: ${{ inputs.releasetype }}
89 | tag_name: ${{ inputs.releasetag }}
90 | make_latest: ${{ inputs.islatest }}
91 | # body: 'Some release text body if needed'
92 | # body_path: or/as/file.md
93 | files: |
94 | app/build/outputs/apk/release/rpcs3-release.apk
95 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | .cache
17 | **/build*/
18 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "app/src/main/cpp/rpcs3"]
2 | path = app/src/main/cpp/rpcs3
3 | url = ../../DHrpcs3/rpcs3.git
4 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | RPCS3
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/render.experimental.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # RPCS3-Android
4 |
5 | *An experimental [RPCS3](https://github.com/RPCS3/rpcs3) emulator port to Android*
6 |
7 | > **This project is discontinued. Alpha-7 is last release**
8 | > Fork of RPCS3 is merged with [RPCSX](https://github.com/RPCSX/rpcsx)
9 | > RPCS3-Android is reuploaded as [RPCSX Android UI](https://github.com/RPCSX/rpcsx-ui-android)
10 |
11 |
12 |
13 | ## Requirements
14 |
15 | Android 12+
16 |
17 |
18 | ## License
19 |
20 | RPCS3-Android is licensed under GPLv2 license except directories containing their own LICENSE file, or files containing their own license.
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.compose.compiler)
5 | id("org.jetbrains.kotlin.plugin.serialization")
6 | id("kotlin-parcelize")
7 | }
8 |
9 | android {
10 | namespace = "net.rpcs3"
11 | compileSdk = 35
12 |
13 | defaultConfig {
14 | applicationId = "net.rpcs3"
15 | minSdk = 31
16 | targetSdk = 35
17 | versionCode = 1
18 | versionName = "1.0"
19 |
20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
21 | ndk {
22 | abiFilters += listOf("arm64-v8a" /*, "x86_64" */)
23 | }
24 | }
25 |
26 | signingConfigs {
27 | create("custom-key") {
28 | val keystoreAlias = System.getenv("KEYSTORE_ALIAS") ?: ""
29 | val keystorePassword = System.getenv("KEYSTORE_PASSWORD") ?: ""
30 | val keystorePath = System.getenv("KEYSTORE_PATH") ?: ""
31 |
32 | if (keystorePath.isNotEmpty() && file(keystorePath).exists() && file(keystorePath).length() > 0) {
33 | keyAlias = keystoreAlias
34 | keyPassword = keystorePassword
35 | storeFile = file(keystorePath)
36 | storePassword = keystorePassword
37 | } else {
38 | val debugKeystoreFile = file("${System.getProperty("user.home")}/debug.keystore")
39 |
40 | println("⚠️ Custom keystore not found or empty! creating debug keystore.")
41 |
42 | if (!debugKeystoreFile.exists()) {
43 | Runtime.getRuntime().exec(
44 | arrayOf(
45 | "keytool", "-genkeypair",
46 | "-v", "-keystore", debugKeystoreFile.absolutePath,
47 | "-storepass", "android",
48 | "-keypass", "android",
49 | "-alias", "androiddebugkey",
50 | "-keyalg", "RSA",
51 | "-keysize", "2048",
52 | "-validity", "10000",
53 | "-dname", "CN=Android Debug,O=Android,C=US"
54 | )
55 | ).waitFor()
56 | }
57 |
58 | keyAlias = "androiddebugkey"
59 | keyPassword = "android"
60 | storeFile = debugKeystoreFile
61 | storePassword = "android"
62 | }
63 | }
64 | }
65 |
66 | buildTypes {
67 | release {
68 | isMinifyEnabled = true
69 | isShrinkResources = true
70 | proguardFiles(
71 | getDefaultProguardFile("proguard-android-optimize.txt"),
72 | "proguard-rules.pro"
73 | )
74 | signingConfig = signingConfigs.getByName("custom-key") ?: signingConfigs.getByName("debug")
75 | }
76 | }
77 |
78 | compileOptions {
79 | sourceCompatibility = JavaVersion.VERSION_11
80 | targetCompatibility = JavaVersion.VERSION_11
81 | }
82 |
83 | kotlinOptions {
84 | jvmTarget = "11"
85 | }
86 |
87 | externalNativeBuild {
88 | cmake {
89 | path = file("src/main/cpp/CMakeLists.txt")
90 | version = "3.31.6"
91 | }
92 | }
93 |
94 | buildFeatures {
95 | viewBinding = true
96 | compose = true
97 | }
98 |
99 | composeOptions {
100 | kotlinCompilerExtensionVersion = "1.5.15"
101 | }
102 |
103 | packaging {
104 | // This is necessary for libadrenotools custom driver loading
105 | jniLibs.useLegacyPackaging = true
106 | }
107 | }
108 |
109 | base.archivesName = "rpcs3"
110 |
111 | dependencies {
112 | implementation(libs.androidx.navigation.compose)
113 | implementation(libs.androidx.ui.tooling.preview.android)
114 | val composeBom = platform("androidx.compose:compose-bom:2025.02.00")
115 | implementation(composeBom)
116 | implementation(libs.androidx.material3)
117 | implementation(libs.androidx.core.ktx)
118 | implementation(libs.androidx.activity.compose)
119 | implementation(libs.androidx.appcompat)
120 | implementation(libs.material)
121 | implementation(libs.androidx.constraintlayout)
122 | implementation(libs.androidx.activity)
123 | testImplementation(libs.junit)
124 | androidTestImplementation(libs.androidx.junit)
125 | androidTestImplementation(libs.androidx.espresso.core)
126 | debugImplementation(libs.androidx.ui.tooling)
127 | implementation(libs.kotlinx.serialization.json)
128 | implementation(libs.coil.compose)
129 | implementation("io.ktor:ktor-client-core:3.0.3")
130 | implementation("io.ktor:ktor-client-cio:3.0.3")
131 | implementation("io.ktor:ktor-client-json:3.0.3")
132 | implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.3")
133 | implementation("io.ktor:ktor-client-content-negotiation:3.0.3")
134 | implementation("io.ktor:ktor-client-logging:3.0.3")
135 | }
136 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/net/rpcs3/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("net.rpcs3", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
29 |
30 |
34 |
35 |
36 |
39 |
40 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
58 |
59 |
60 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/app/src/main/cpp/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.16.9)
2 | project("rpcs3-android")
3 |
4 | set(CMAKE_CXX_STANDARD 20)
5 | set(CMAKE_POSITION_INDEPENDENT_CODE on)
6 |
7 | set(FFMPEG_VERSION 5.1)
8 | set(LLVM_VERSION 19.1)
9 |
10 | if (TEST_OVERRIDE_CPU)
11 | if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64")
12 | set(ARCH_FLAGS "-mcpu=cortex-a53")
13 | else()
14 | set(ARCH_FLAGS "-mno-avx")
15 | endif()
16 |
17 |
18 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${ARCH_FLAGS}")
19 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${ARCH_FLAGS}")
20 | set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${ARCH_FLAGS}")
21 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${ARCH_FLAGS}")
22 | endif()
23 |
24 | if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64")
25 | set(RPCS3_DOWNLOAD_ARCH "arm64-v8a")
26 | else()
27 | set(RPCS3_DOWNLOAD_ARCH "x86-64")
28 | endif()
29 |
30 | if(NOT EXISTS ${CMAKE_BINARY_DIR}/ffmpeg-${FFMPEG_VERSION}.tar.gz)
31 | message(STATUS "Downloading ffmpeg-${FFMPEG_VERSION}")
32 | file(DOWNLOAD
33 | https://github.com/RPCS3-Android/ffmpeg-android/releases/download/${FFMPEG_VERSION}/ffmpeg-${RPCS3_DOWNLOAD_ARCH}-Android.tar.gz
34 | ${CMAKE_BINARY_DIR}/ffmpeg-${FFMPEG_VERSION}.tar.gz
35 | SHOW_PROGRESS
36 | )
37 | endif()
38 |
39 | set(FFMPEG_PATH "${CMAKE_BINARY_DIR}/ffmpeg-${FFMPEG_VERSION}")
40 |
41 | make_directory(${FFMPEG_PATH})
42 | add_custom_command(
43 | OUTPUT ${FFMPEG_PATH}/src
44 | COMMAND ${CMAKE_COMMAND} -E tar xzf ${CMAKE_BINARY_DIR}/ffmpeg-${FFMPEG_VERSION}.tar.gz
45 | DEPENDS ${CMAKE_BINARY_DIR}/ffmpeg-${FFMPEG_VERSION}.tar.gz
46 | WORKING_DIRECTORY ${FFMPEG_PATH}
47 | COMMENT "Unpacking ${CMAKE_BINARY_DIR}/ffmpeg-${FFMPEG_VERSION}.tar.gz"
48 | )
49 |
50 | add_custom_target(ffmpeg-unpack DEPENDS ${FFMPEG_PATH}/src)
51 |
52 | function(import_ffmpeg_library name)
53 | add_custom_command(
54 | OUTPUT "${FFMPEG_PATH}/lib${name}/lib${name}.a"
55 | DEPENDS ffmpeg-unpack
56 | WORKING_DIRECTORY ${FFMPEG_PATH}
57 | )
58 |
59 | add_custom_target(ffmpeg-unpack-${name} DEPENDS "${FFMPEG_PATH}/lib${name}/lib${name}.a")
60 | add_library(ffmpeg::${name} STATIC IMPORTED GLOBAL)
61 | set_property(TARGET ffmpeg::${name} PROPERTY IMPORTED_LOCATION "${FFMPEG_PATH}/lib${name}/lib${name}.a")
62 | set_property(TARGET ffmpeg::${name} PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${FFMPEG_PATH}" "${FFMPEG_PATH}/src")
63 | add_dependencies(ffmpeg::${name} ffmpeg-unpack-${name})
64 | endfunction()
65 |
66 | import_ffmpeg_library(avcodec)
67 | import_ffmpeg_library(avformat)
68 | import_ffmpeg_library(avfilter)
69 | import_ffmpeg_library(avdevice)
70 | import_ffmpeg_library(avutil)
71 | import_ffmpeg_library(swscale)
72 | import_ffmpeg_library(swresample)
73 |
74 | add_library(3rdparty_ffmpeg INTERFACE)
75 | target_link_libraries(3rdparty_ffmpeg INTERFACE
76 | ffmpeg::avformat
77 | ffmpeg::avcodec
78 | ffmpeg::avutil
79 | ffmpeg::swscale
80 | ffmpeg::swresample
81 | )
82 |
83 | add_dependencies(3rdparty_ffmpeg ffmpeg-unpack)
84 |
85 |
86 | if(NOT EXISTS ${CMAKE_BINARY_DIR}/llvm-${LLVM_VERSION}.tar.gz)
87 | message(STATUS "Downloading llvm-${LLVM_VERSION}")
88 | file(DOWNLOAD
89 | https://github.com/RPCS3-Android/llvm-android/releases/download/${LLVM_VERSION}/llvm-${RPCS3_DOWNLOAD_ARCH}-Android.tar.gz
90 | ${CMAKE_BINARY_DIR}/llvm-${LLVM_VERSION}.tar.gz
91 | SHOW_PROGRESS
92 | )
93 | endif()
94 |
95 | set(LLVM_DIR ${CMAKE_BINARY_DIR}/llvm-${LLVM_VERSION}.7-Android/lib/cmake/llvm)
96 |
97 | if (NOT EXISTS ${LLVM_DIR})
98 | message(STATUS "Unpacking llvm-${LLVM_VERSION}")
99 | execute_process(
100 | COMMAND ${CMAKE_COMMAND} -E tar xzf ${CMAKE_BINARY_DIR}/llvm-${LLVM_VERSION}.tar.gz
101 | WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
102 | )
103 | endif()
104 |
105 | set(USE_SYSTEM_LIBUSB off)
106 | set(USE_SYSTEM_CURL off)
107 | set(USE_DISCORD_RPC off)
108 | set(USE_SYSTEM_OPENCV off)
109 | set(USE_SYSTEM_FFMPEG off)
110 | set(USE_FAUDIO off)
111 | set(USE_SDL2 off)
112 | set(BUILD_LLVM off)
113 | set(STATIC_LINK_LLVM on)
114 | set(DISABLE_LTO on)
115 | set(USE_LTO off)
116 | set(USE_OPENSL off)
117 | set(ASMJIT_NO_SHM_OPEN on)
118 | set(USE_SYSTEM_ZLIB on)
119 | set(USE_LIBEVDEV off)
120 |
121 | add_subdirectory(rpcs3 EXCLUDE_FROM_ALL)
122 |
123 | add_library(${CMAKE_PROJECT_NAME} SHARED
124 | native-lib.cpp
125 | iso.cpp
126 | rpcs3/rpcs3/stb_image.cpp
127 | rpcs3/rpcs3/Input/ds3_pad_handler.cpp
128 | rpcs3/rpcs3/Input/ds4_pad_handler.cpp
129 | rpcs3/rpcs3/Input/dualsense_pad_handler.cpp
130 | rpcs3/rpcs3/Input/evdev_joystick_handler.cpp
131 | rpcs3/rpcs3/Input/evdev_gun_handler.cpp
132 | # rpcs3/rpcs3/Input/gui_pad_thread.cpp
133 | rpcs3/rpcs3/Input/hid_pad_handler.cpp
134 | rpcs3/rpcs3/Input/virtual_pad_handler.cpp
135 | rpcs3/rpcs3/Input/mm_joystick_handler.cpp
136 | rpcs3/rpcs3/Input/pad_thread.cpp
137 | rpcs3/rpcs3/Input/product_info.cpp
138 | rpcs3/rpcs3/Input/ps_move_calibration.cpp
139 | rpcs3/rpcs3/Input/ps_move_config.cpp
140 | rpcs3/rpcs3/Input/ps_move_handler.cpp
141 | rpcs3/rpcs3/Input/ps_move_tracker.cpp
142 | rpcs3/rpcs3/Input/raw_mouse_config.cpp
143 | rpcs3/rpcs3/Input/raw_mouse_handler.cpp
144 | rpcs3/rpcs3/Input/sdl_pad_handler.cpp
145 | rpcs3/rpcs3/Input/skateboard_pad_handler.cpp
146 | rpcs3/rpcs3/rpcs3_version.cpp
147 | )
148 |
149 | target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC rpcs3/rpcs3)
150 |
151 | target_link_libraries(${CMAKE_PROJECT_NAME}
152 | android
153 | log
154 | rpcs3_emu
155 | nativehelper
156 | 3rdparty::libusb
157 | 3rdparty::hidapi
158 | 3rdparty::wolfssl
159 | 3rdparty::libcurl
160 | 3rdparty::zlib
161 | 3rdparty::fusion
162 | )
163 |
--------------------------------------------------------------------------------
/app/src/main/cpp/block_dev.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "Utilities/File.h"
4 | #include
5 | #include
6 |
7 | class block_dev {
8 | std::size_t m_block_size = 0;
9 | std::size_t m_block_count = 0;
10 |
11 | public:
12 | virtual ~block_dev() = default;
13 |
14 | std::size_t block_size() const { return m_block_size; }
15 | std::size_t block_count() const { return m_block_count; }
16 | std::size_t size() const { return block_size() * block_count(); }
17 |
18 | virtual std::size_t read(std::size_t blockIndex, void *data,
19 | std::size_t blockCount) = 0;
20 |
21 | virtual std::size_t write(std::size_t blockIndex, const void *data,
22 | std::size_t blockCount) = 0;
23 |
24 | protected:
25 | void set_block_info(std::size_t size, std::size_t count) {
26 | m_block_size = size;
27 | m_block_count = count;
28 | }
29 | };
30 |
31 | class file_block_dev final : public block_dev {
32 | fs::file m_file;
33 |
34 | public:
35 | explicit file_block_dev(fs::file file, std::size_t blockSize = 2048)
36 | : m_file(std::move(file)) {
37 | set_block_info(blockSize, m_file.size() / blockSize);
38 | }
39 |
40 | std::size_t read(std::size_t blockIndex, void *data,
41 | std::size_t blockCount) override {
42 | auto result = m_file.read_at(block_size() * blockIndex, data,
43 | blockCount * block_size());
44 | return result / block_size();
45 | }
46 |
47 | std::size_t write(std::size_t blockIndex, const void *data,
48 | std::size_t blockCount) override {
49 | auto result = m_file.write_at(block_size() * blockIndex, data,
50 | blockCount * block_size());
51 | return result / block_size();
52 | }
53 |
54 | fs::file &file() { return m_file; }
55 | fs::file release() { return std::exchange(m_file, {}); }
56 | };
57 |
58 | class file_view_block_dev final : public block_dev {
59 | const fs::file *m_file;
60 |
61 | public:
62 | explicit file_view_block_dev(const fs::file &file,
63 | std::size_t blockSize = 2048)
64 | : m_file(&file) {
65 | set_block_info(blockSize, m_file->size() / blockSize);
66 | }
67 |
68 | std::size_t read(std::size_t blockIndex, void *data,
69 | std::size_t blockCount) override {
70 | auto result = m_file->read_at(block_size() * blockIndex, data,
71 | blockCount * block_size());
72 | return result / block_size();
73 | }
74 |
75 | std::size_t write(std::size_t blockIndex, const void *data,
76 | std::size_t blockCount) override {
77 | auto result = m_file->write_at(block_size() * blockIndex, data,
78 | blockCount * block_size());
79 | return result / block_size();
80 | }
81 | };
82 |
--------------------------------------------------------------------------------
/app/src/main/cpp/compile_commands.json:
--------------------------------------------------------------------------------
1 | ../../../.cxx/tools/debug/arm64-v8a/compile_commands.json
--------------------------------------------------------------------------------
/app/src/main/cpp/fs_provider.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "Utilities/File.h"
4 | #include
5 | #include
6 |
7 | class fs_provider {
8 | public:
9 | enum class handle : std::uintptr_t {
10 | invalid = ~static_cast(0)
11 | };
12 |
13 | virtual ~fs_provider() = default;
14 | virtual fs::file open(const std::filesystem::path &path,
15 | fs::open_mode mode = fs::open_mode::read) = 0;
16 | virtual fs::dir open_dir(const std::filesystem::path &path) = 0;
17 | };
18 |
--------------------------------------------------------------------------------
/app/src/main/cpp/iso.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "Utilities/File.h"
4 | #include "block_dev.hpp"
5 | #include "fs_provider.hpp"
6 | #include "util/endian.hpp"
7 | #include "util/types.hpp"
8 | #include
9 | #include
10 |
11 | namespace iso {
12 | #pragma pack(push, 1)
13 | template struct le_be_pair {
14 | le_t le;
15 | be_t be;
16 |
17 | T value() const {
18 | if constexpr (std::endian::native == std::endian::little) {
19 | return le;
20 | } else {
21 | return be;
22 | }
23 | }
24 | };
25 |
26 | struct PrimaryVolumeDescriptorDateTime {
27 | char year[4];
28 | char month[2];
29 | char day[2];
30 | char hour[2];
31 | char minute[2];
32 | char second[2];
33 | char milliseconds[2];
34 | s8 gmt_offset;
35 | };
36 |
37 | struct VolumeHeader {
38 | u8 type;
39 | char standard_id[5];
40 | u8 version;
41 | };
42 |
43 | struct DirDateTime {
44 | u8 year; // + 1900
45 | u8 month;
46 | u8 day;
47 | u8 hour;
48 | u8 minute;
49 | u8 second;
50 | s8 gmt_offset;
51 |
52 | struct tm to_tm() const {
53 | struct tm time{};
54 | time.tm_year = year;
55 | time.tm_mon = month;
56 | time.tm_mday = day;
57 | time.tm_hour = hour;
58 | time.tm_min = minute;
59 | time.tm_sec = second;
60 | time.tm_gmtoff = gmt_offset;
61 | return time;
62 | }
63 |
64 | time_t to_time_t() const {
65 | auto tm = to_tm();
66 | return mktime(&tm);
67 | }
68 | };
69 |
70 | enum class DirEntryFlags : u8 {
71 | None = 0,
72 | Hidden = 1 << 0,
73 | Directory = 1 << 1,
74 | File = 1 << 2,
75 | ExtAttr = 1 << 3,
76 | Permissions = 1 << 4,
77 | };
78 |
79 | constexpr DirEntryFlags operator&(DirEntryFlags lhs, DirEntryFlags rhs) {
80 | return static_cast(static_cast(lhs) &
81 | static_cast(rhs));
82 | }
83 | constexpr DirEntryFlags operator|(DirEntryFlags lhs, DirEntryFlags rhs) {
84 | return static_cast(static_cast(lhs) |
85 | static_cast(rhs));
86 | }
87 |
88 | struct DirEntry {
89 | u8 entry_length;
90 | u8 ext_attr_length;
91 | le_be_pair lba;
92 | le_be_pair length;
93 | DirDateTime create_time;
94 | DirEntryFlags flags;
95 | u8 interleave_unit_size;
96 | u8 interleave_gap_size;
97 | le_be_pair sequence;
98 | u8 filename_length;
99 |
100 | fs::stat_t to_fs_stat() const {
101 | fs::stat_t result{};
102 | result.is_directory =
103 | (flags & iso::DirEntryFlags::Directory) != iso::DirEntryFlags::None;
104 | result.size = length.value();
105 | result.ctime = create_time.to_time_t();
106 | result.mtime = result.ctime;
107 | result.atime = result.ctime;
108 | return result;
109 | }
110 | fs::dir_entry to_fs_entry(std::string name) const {
111 | fs::dir_entry entry = {};
112 | static_cast(entry) = to_fs_stat();
113 | entry.name = std::move(name);
114 | return entry;
115 | }
116 | };
117 |
118 | struct PrimaryVolumeDescriptor {
119 | VolumeHeader header;
120 | uint8_t pad0;
121 | char system_id[32];
122 | char volume_id[32];
123 | char pad1[8];
124 | le_be_pair block_count;
125 | char pad2[32];
126 | le_be_pair volume_set_size;
127 | le_be_pair vol_seq_num;
128 | le_be_pair block_size;
129 | le_be_pair path_table_size;
130 | le_t path_table_block_le;
131 | le_t ext_path_table_block_le;
132 | be_t path_table_block_be;
133 | be_t ext_path_table_block_be;
134 | DirEntry root;
135 | u8 pad3;
136 | uint8_t volume_set_id[128];
137 | uint8_t publisher_id[128];
138 | uint8_t data_preparer_id[128];
139 | uint8_t application_id[128];
140 | uint8_t copyright_file_id[37];
141 | uint8_t abstract_file_id[37];
142 | uint8_t bibliographical_file_id[37];
143 | PrimaryVolumeDescriptorDateTime vol_creation_time;
144 | PrimaryVolumeDescriptorDateTime vol_modification_time;
145 | PrimaryVolumeDescriptorDateTime vol_expire_time;
146 | PrimaryVolumeDescriptorDateTime vol_effective_time;
147 | uint8_t version;
148 | uint8_t pad4;
149 | uint8_t app_used[512];
150 |
151 | u32 path_table_block() const {
152 | if constexpr (std::endian::native == std::endian::little) {
153 | return path_table_block_le;
154 | } else {
155 | return path_table_block_be;
156 | }
157 | }
158 | };
159 |
160 | struct PathTableEntryHeader {
161 | u8 name_length;
162 | u8 ext_attr_length;
163 | le_t location;
164 | le_t parent_id;
165 | };
166 |
167 | enum class StringEncoding {
168 | ascii,
169 | utf16_be,
170 | };
171 |
172 | #pragma pack(pop)
173 | } // namespace iso
174 |
175 | class iso_fs final : public fs_provider {
176 | std::unique_ptr m_dev;
177 | iso::DirEntry m_root_dir;
178 | iso::StringEncoding m_encoding = iso::StringEncoding::ascii;
179 |
180 | public:
181 | iso_fs() = default;
182 |
183 | static std::optional open(std::unique_ptr device) {
184 | iso_fs result;
185 | result.m_dev = std::move(device);
186 |
187 | if (!result.initialize()) {
188 | return {};
189 | }
190 |
191 | return result;
192 | }
193 |
194 | fs::file open(const std::filesystem::path &path,
195 | fs::open_mode mode = ::fs::open_mode::read) override;
196 | fs::dir open_dir(const std::filesystem::path &path) override;
197 |
198 | private:
199 | bool initialize();
200 |
201 | std::optional open_entry(const std::filesystem::path &path,
202 | bool isDir);
203 |
204 | std::pair, std::vector>
205 | read_dir(const iso::DirEntry &entry);
206 | fs::file read_file(const iso::DirEntry &entry);
207 | };
208 |
--------------------------------------------------------------------------------
/app/src/main/ic_rpcs3-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/ic_rpcs3-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/FirmwareRepository.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3
2 |
3 | import android.content.res.Resources.NotFoundException
4 | import androidx.compose.runtime.MutableState
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.annotation.Keep
7 | import kotlinx.serialization.Serializable
8 | import kotlinx.serialization.json.Json
9 | import java.io.File
10 |
11 |
12 | enum class FirmwareStatus {
13 | None,
14 | Installed,
15 | Compiled
16 | }
17 |
18 | @Serializable
19 | private data class FirmwareInfo(val version: String?, val status: FirmwareStatus);
20 |
21 | class FirmwareRepository {
22 | companion object {
23 | val progressChannel: MutableState = mutableStateOf(null)
24 | val version: MutableState = mutableStateOf(null)
25 | val status: MutableState = mutableStateOf(FirmwareStatus.None)
26 |
27 | fun save() {
28 | try {
29 | File(RPCS3.rootDirectory + "fw.json").writeText(
30 | Json.encodeToString(
31 | FirmwareInfo(version.value, status.value)
32 | )
33 | )
34 | } catch (e: Exception) {
35 | e.printStackTrace()
36 |
37 | }
38 | }
39 |
40 | fun load() {
41 | try {
42 | val info =
43 | Json.decodeFromString(File(RPCS3.rootDirectory + "fw.json").readText())
44 | status.value = info.status
45 | version.value = info.version
46 | } catch (_: NotFoundException) {
47 | } catch (e: Exception) {
48 | e.printStackTrace()
49 | }
50 | }
51 |
52 | @Keep
53 | @JvmStatic
54 | fun onFirmwareInstalled(version: String?) {
55 | updateStatus(version, FirmwareStatus.Installed)
56 | }
57 |
58 | @Keep
59 | @JvmStatic
60 | fun onFirmwareCompiled(version: String?) {
61 | updateStatus(version, FirmwareStatus.Compiled)
62 | }
63 |
64 | fun updateStatus(version: String?, status: FirmwareStatus) {
65 | synchronized(Companion.version) {
66 | Companion.version.value = version
67 | Companion.status.value = status
68 |
69 | save()
70 | }
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/GraphicsFrame.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.view.SurfaceHolder
6 | import android.view.SurfaceView
7 | import kotlin.concurrent.thread
8 |
9 | class GraphicsFrame : SurfaceView, SurfaceHolder.Callback {
10 | constructor(context: Context) : super(context) {
11 | holder.addCallback(this)
12 | }
13 |
14 | constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
15 | holder.addCallback(this)
16 | }
17 |
18 | constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
19 | context,
20 | attrs,
21 | defStyleAttr
22 | ) {
23 | holder.addCallback(this)
24 | }
25 |
26 | constructor(
27 | context: Context?,
28 | attrs: AttributeSet?,
29 | defStyleAttr: Int,
30 | defStyleRes: Int
31 | ) : super(context, attrs, defStyleAttr, defStyleRes) {
32 | holder.addCallback(this)
33 | }
34 |
35 | override fun surfaceCreated(p0: SurfaceHolder) {
36 | RPCS3.instance.surfaceEvent(p0.surface, 0)
37 | }
38 |
39 | override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) {
40 | RPCS3.instance.surfaceEvent(p0.surface, 1)
41 | }
42 |
43 | override fun surfaceDestroyed(p0: SurfaceHolder) {
44 | RPCS3.instance.surfaceEvent(p0.surface, 2)
45 | }
46 | }
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3
2 |
3 | import android.app.Notification
4 | import android.app.NotificationChannel
5 | import android.app.NotificationManager
6 | import android.os.Bundle
7 | import androidx.activity.ComponentActivity
8 | import androidx.activity.compose.setContent
9 | import androidx.core.view.WindowCompat
10 | import androidx.lifecycle.lifecycleScope
11 | import kotlinx.coroutines.launch
12 | import net.rpcs3.ui.navigation.AppNavHost
13 | import kotlin.concurrent.thread
14 |
15 | private const val ACTION_USB_PERMISSION = "net.rpcs3.USB_PERMISSION"
16 |
17 | class MainActivity : ComponentActivity() {
18 | private lateinit var unregisterUsbEventListener: () -> Unit
19 | override fun onCreate(savedInstanceState: Bundle?) {
20 | super.onCreate(savedInstanceState)
21 |
22 | WindowCompat.setDecorFitsSystemWindows(window, false)
23 |
24 | setContent {
25 | RPCS3Theme {
26 | AppNavHost()
27 | }
28 | }
29 |
30 | RPCS3.rootDirectory = applicationContext.getExternalFilesDir(null).toString()
31 | if (!RPCS3.rootDirectory.endsWith("/")) {
32 | RPCS3.rootDirectory += "/"
33 | }
34 |
35 | if (!RPCS3.initialized) {
36 | lifecycleScope.launch { GameRepository.load() }
37 | FirmwareRepository.load()
38 | }
39 |
40 | Permission.PostNotifications.requestPermission(this)
41 |
42 | with(getSystemService(NOTIFICATION_SERVICE) as NotificationManager) {
43 | val channel = NotificationChannel(
44 | "rpcs3-progress",
45 | "Installation progress",
46 | NotificationManager.IMPORTANCE_DEFAULT
47 | ).apply {
48 | setShowBadge(false)
49 | lockscreenVisibility = Notification.VISIBILITY_PUBLIC
50 | }
51 |
52 | createNotificationChannel(channel)
53 | }
54 |
55 | if (!RPCS3.initialized) {
56 | RPCS3.instance.initialize(RPCS3.rootDirectory)
57 | val nativeLibraryDir = packageManager.getApplicationInfo(packageName, 0).nativeLibraryDir
58 | RPCS3.instance.settingsSet("Video@@Vulkan@@Custom Driver@@Hook Directory", "\"" + nativeLibraryDir + "\"")
59 | RPCS3.initialized = true
60 |
61 | thread {
62 | RPCS3.instance.startMainThreadProcessor()
63 | }
64 |
65 | thread {
66 | RPCS3.instance.processCompilationQueue()
67 | }
68 | }
69 |
70 | unregisterUsbEventListener = listenUsbEvents(this)
71 | }
72 |
73 | override fun onDestroy() {
74 | super.onDestroy()
75 | unregisterUsbEventListener()
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/Permission.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3
2 |
3 | import android.Manifest
4 | import android.annotation.SuppressLint
5 | import android.app.Activity
6 | import android.content.Context
7 | import android.content.pm.PackageManager
8 | import androidx.core.app.ActivityCompat
9 | import androidx.core.content.ContextCompat
10 |
11 | enum class Permission(val id: Int, val key: String) {
12 | @SuppressLint("InlinedApi")
13 | PostNotifications(100, Manifest.permission.POST_NOTIFICATIONS);
14 |
15 | fun checkPermission(context: Context) =
16 | ContextCompat.checkSelfPermission(context, key) == PackageManager.PERMISSION_GRANTED
17 |
18 | fun requestPermission(activity: Activity) {
19 | if (!checkPermission(activity)) {
20 | ActivityCompat.requestPermissions(activity, arrayOf(key), id)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/PrecompilerService.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3
2 |
3 | import android.app.Service
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.pm.ServiceInfo
7 | import android.net.Uri
8 | import android.os.Build
9 | import android.os.IBinder
10 | import androidx.core.app.NotificationCompat
11 | import androidx.core.app.ServiceCompat
12 | import kotlin.concurrent.thread
13 |
14 | enum class PrecompilerServiceAction {
15 | InstallFirmware,
16 | Install
17 | }
18 |
19 | class PrecompilerService : Service() {
20 | companion object {
21 | fun start(context: Context, action: PrecompilerServiceAction, uri: Uri?) {
22 | val intent = Intent(context, PrecompilerService::class.java)
23 | intent.putExtra("action", action.ordinal)
24 | intent.putExtra("uri", uri)
25 |
26 | try {
27 | context.startForegroundService(intent)
28 | } catch (e: Exception) {
29 | e.printStackTrace()
30 | }
31 | }
32 |
33 | fun start(context: Context, action: PrecompilerServiceAction, batch: ArrayList) {
34 | if (batch.isEmpty()) {
35 | return
36 | }
37 |
38 | if (batch.size == 1) {
39 | start(context, action, batch[0])
40 | return
41 | }
42 |
43 | val intent = Intent(context, PrecompilerService::class.java)
44 | intent.putExtra("action", action.ordinal)
45 | intent.putExtra("batch", batch)
46 |
47 | try {
48 | context.startForegroundService(intent)
49 | } catch (e: Exception) {
50 | e.printStackTrace()
51 | }
52 | }
53 | }
54 |
55 | override fun onBind(intent: Intent?): IBinder? {
56 | return null
57 | }
58 |
59 | override fun onCreate() {
60 | super.onCreate()
61 | }
62 |
63 | fun install(isFw: Boolean, uri: Uri, installProgress: Long): Boolean {
64 | val descriptor = contentResolver.openAssetFileDescriptor(uri, "r")
65 | val fd = descriptor?.parcelFileDescriptor?.fd
66 |
67 | if (fd == null) {
68 | try {
69 | descriptor?.close()
70 | } catch (e: Exception) {
71 | e.printStackTrace()
72 | }
73 |
74 | return false
75 | }
76 |
77 | val installResult =
78 | if (isFw)
79 | RPCS3.instance.installFw(fd, installProgress)
80 | else
81 | RPCS3.instance.install(fd, installProgress)
82 |
83 | if (!installResult) {
84 | try {
85 | ProgressRepository.onProgressEvent(installProgress, -1, 0)
86 | } catch (e: Exception) {
87 | e.printStackTrace()
88 | }
89 | }
90 |
91 | try {
92 | descriptor.close()
93 | } catch (e: Exception) {
94 | e.printStackTrace()
95 | }
96 |
97 | return true
98 | }
99 |
100 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
101 | val batch = intent?.getParcelableArrayListExtra("batch")
102 | val uri = intent?.getParcelableExtra("uri")
103 | val action = intent?.getIntExtra("action", 0)
104 | val isFwInstall = action == PrecompilerServiceAction.InstallFirmware.ordinal
105 |
106 | if (uri == null && batch == null) {
107 | stopSelf(startId)
108 | return START_NOT_STICKY
109 | }
110 |
111 | val installProgress =
112 | ProgressRepository.create(
113 | this,
114 | if (isFwInstall) "Firmware Installation" else "Package Installation"
115 | ) { entry ->
116 | if (entry.isFinished()) {
117 | if (isFwInstall) {
118 | FirmwareRepository.progressChannel.value = null
119 | }
120 |
121 | stopSelf(startId)
122 | }
123 | }
124 |
125 | if (isFwInstall) {
126 | FirmwareRepository.progressChannel.value = installProgress
127 | }
128 |
129 | try {
130 | ServiceCompat.startForeground(
131 | this,
132 | installProgress.toInt(),
133 | NotificationCompat.Builder(this, "rpcs3-progress").build(),
134 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
135 | ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
136 | } else {
137 | 0
138 | }
139 | )
140 | } catch (e: Exception) {
141 | e.printStackTrace()
142 | }
143 |
144 | thread {
145 | var installResult = false
146 | if (uri != null) {
147 | installResult = install(isFwInstall, uri, installProgress)
148 | } else batch?.forEach { uri ->
149 | // FIXME: create child progress
150 | if (install(isFwInstall, uri, installProgress)) {
151 | installResult = true
152 | }
153 | }
154 |
155 | if (!installResult) {
156 | stopSelf(startId)
157 | }
158 | }
159 |
160 | return START_STICKY
161 | }
162 | }
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/ProgressRepository.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import android.os.Handler
6 | import android.os.Looper
7 | import android.os.Message
8 | import androidx.annotation.Keep
9 | import androidx.compose.runtime.MutableLongState
10 | import androidx.compose.runtime.MutableState
11 | import androidx.compose.runtime.mutableLongStateOf
12 | import androidx.compose.runtime.mutableStateOf
13 | import androidx.core.app.NotificationCompat
14 | import androidx.core.app.NotificationManagerCompat
15 | import net.rpcs3.dialogs.AlertDialogQueue
16 | import java.util.concurrent.ConcurrentHashMap
17 |
18 | data class ProgressEntry(
19 | val value: MutableLongState = mutableLongStateOf(0),
20 | val max: MutableLongState = mutableLongStateOf(0),
21 | val message: MutableState = mutableStateOf(null)
22 | ) {
23 | fun isComplete() = value.longValue == max.longValue && !isIndeterminate()
24 | fun isFailed() = value.longValue < 0
25 | fun isFinished() = isFailed() || isComplete()
26 | fun isIndeterminate() = max.longValue == 0L
27 | }
28 |
29 | data class ProgressUpdateEntry(val value: Long, val max: Long, val message: String?) {
30 | fun isComplete() = value == max && !isIndeterminate()
31 | fun isFailed() = value < 0
32 | fun isFinished() = isFailed() || isComplete()
33 | fun isIndeterminate() = max == 0L
34 | }
35 |
36 | private data class ProgressWithHandler(
37 | var handler: (ProgressUpdateEntry) -> Unit,
38 | val progressEntry: MutableState
39 | )
40 |
41 | class ProgressRepository {
42 | private var progressHandlers = ConcurrentHashMap()
43 | private var nextRequestId = 1L
44 |
45 | companion object {
46 | private val instance = ProgressRepository()
47 |
48 | fun getItem(id: Long?) =
49 | if (id != null) instance.progressHandlers[id]?.progressEntry else null
50 |
51 | @Keep
52 | @JvmStatic
53 | fun onProgressEvent(id: Long, value: Long, max: Long, message: String? = null): Boolean {
54 | val item = instance.progressHandlers[id] ?: return false
55 |
56 | item.progressEntry.value.apply {
57 | this.value.longValue = value
58 | this.max.longValue = max
59 | this.message.value = message ?: this.message.value
60 | }
61 |
62 | item.handler(ProgressUpdateEntry(value, max, item.progressEntry.value.message.value))
63 |
64 | if (item.progressEntry.value.isFinished()) {
65 | cancel(id)
66 | }
67 |
68 | return true
69 | }
70 |
71 | fun cancel(id: Long) {
72 | instance.progressHandlers.remove(id)
73 | GameRepository.clearProgress(id)
74 | }
75 |
76 | fun create(
77 | context: Context,
78 | title: String,
79 | silent: Boolean = false,
80 | handler: (ProgressUpdateEntry) -> Unit = { _ -> }
81 | ): Long {
82 | var requestId: Long
83 | val entry = ProgressWithHandler(handler, mutableStateOf(ProgressEntry()))
84 | while (true) {
85 | requestId = instance.nextRequestId++
86 | if (instance.progressHandlers.put(requestId, entry) == null) {
87 | break
88 | }
89 | }
90 |
91 | val hasPermission = !silent && Permission.PostNotifications.checkPermission(context)
92 |
93 | val builder = NotificationCompat.Builder(context, "rpcs3-progress").apply {
94 | setContentTitle(title)
95 | setSmallIcon(R.drawable.ic_rpcs3_monochrome)
96 | setCategory(NotificationCompat.CATEGORY_SERVICE)
97 | setPriority(NotificationCompat.PRIORITY_DEFAULT)
98 | setProgress(0, 0, true)
99 | setSilent(true)
100 | }
101 |
102 | if (hasPermission) {
103 | with(NotificationManagerCompat.from(context)) {
104 | notify(requestId.toInt(), builder.build())
105 | }
106 | }
107 |
108 | val asyncHandler = Handler.createAsync(Looper.getMainLooper()) { message ->
109 | val value = message.data.getLong("value")
110 | val max = message.data.getLong("max")
111 | val text = message.data.getString("message")
112 |
113 | if (hasPermission) {
114 | val notificationManager = NotificationManagerCompat.from(context)
115 |
116 | if (text != null) {
117 | builder.setContentText(text)
118 | }
119 |
120 | if (value >= 0 && max > 0) {
121 | if (value == max) {
122 | notificationManager.cancel(requestId.toInt())
123 | } else {
124 | builder.setProgress(max.toInt(), value.toInt(), false)
125 | notificationManager.notify(requestId.toInt(), builder.build())
126 | }
127 | } else if (value < 0) {
128 | val contentText = text ?: "Installation failed"
129 | builder.setContentText(contentText)
130 | AlertDialogQueue.showDialog(title, contentText)
131 | notificationManager.notify(requestId.toInt(), builder.build())
132 | } else {
133 | builder.setProgress(max.toInt(), value.toInt(), true)
134 | notificationManager.notify(requestId.toInt(), builder.build())
135 | }
136 | }
137 |
138 | handler(ProgressUpdateEntry(value, max, text))
139 | true
140 | }
141 |
142 | entry.handler = { progress: ProgressUpdateEntry ->
143 | val message = Message()
144 | val data = Bundle()
145 | data.putLong("value", progress.value)
146 | data.putLong("max", progress.max)
147 | data.putString("message", progress.message)
148 | message.data = data
149 | asyncHandler.sendMessage(message)
150 | }
151 |
152 | return requestId
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/RPCS3.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3
2 |
3 | import android.view.Surface
4 | import androidx.compose.runtime.mutableStateOf
5 |
6 | enum class Digital1Flags(val bit: Int)
7 | {
8 | None(0),
9 | CELL_PAD_CTRL_SELECT(0x00000001),
10 | CELL_PAD_CTRL_L3(0x00000002),
11 | CELL_PAD_CTRL_R3(0x00000004),
12 | CELL_PAD_CTRL_START(0x00000008),
13 | CELL_PAD_CTRL_UP(0x00000010),
14 | CELL_PAD_CTRL_RIGHT(0x00000020),
15 | CELL_PAD_CTRL_DOWN(0x00000040),
16 | CELL_PAD_CTRL_LEFT(0x00000080),
17 | CELL_PAD_CTRL_PS(0x00000100),
18 | }
19 |
20 | enum class Digital2Flags(val bit: Int)
21 | {
22 | None(0),
23 | CELL_PAD_CTRL_L2(0x00000001),
24 | CELL_PAD_CTRL_R2(0x00000002),
25 | CELL_PAD_CTRL_L1(0x00000004),
26 | CELL_PAD_CTRL_R1(0x00000008),
27 | CELL_PAD_CTRL_TRIANGLE(0x00000010),
28 | CELL_PAD_CTRL_CIRCLE(0x00000020),
29 | CELL_PAD_CTRL_CROSS(0x00000040),
30 | CELL_PAD_CTRL_SQUARE(0x00000080),
31 | };
32 |
33 | enum class EmulatorState {
34 | Stopped,
35 | Loading,
36 | Stopping,
37 | Running,
38 | Paused,
39 | Frozen, // paused but cannot resume
40 | Ready,
41 | Starting;
42 |
43 | companion object {
44 | fun fromInt(value: Int) = EmulatorState.entries.first { it.ordinal == value }
45 | }
46 | }
47 |
48 | enum class BootResult
49 | {
50 | NoErrors,
51 | GenericError,
52 | NothingToBoot,
53 | WrongDiscLocation,
54 | InvalidFileOrFolder,
55 | InvalidBDvdFolder,
56 | InstallFailed,
57 | DecryptionError,
58 | FileCreationError,
59 | FirmwareMissing,
60 | UnsupportedDiscType,
61 | SavestateCorrupted,
62 | SavestateVersionUnsupported,
63 | StillRunning,
64 | AlreadyAdded,
65 | CurrentlyRestricted;
66 |
67 | companion object {
68 | fun fromInt(value: Int) = entries.first { it.ordinal == value }
69 | }
70 | };
71 |
72 | class RPCS3 {
73 | external fun initialize(rootDir: String): Boolean
74 | external fun installFw(fd: Int, progressId: Long): Boolean
75 | external fun install(fd: Int, progressId: Long): Boolean
76 | external fun installKey(fd: Int, requestId: Long, gamePath: String): Boolean
77 | external fun boot(path: String): Int
78 | external fun surfaceEvent(surface: Surface, event: Int): Boolean
79 | external fun usbDeviceEvent(fd: Int, vendorId: Int, productId: Int, event: Int): Boolean
80 | external fun processCompilationQueue(): Boolean
81 | external fun startMainThreadProcessor(): Boolean
82 | external fun overlayPadData(digital1: Int, digital2: Int, leftStickX: Int, leftStickY: Int, rightStickX: Int, rightStickY: Int): Boolean
83 | external fun collectGameInfo(rootDir: String, progressId: Long): Boolean
84 | external fun systemInfo(): String
85 | external fun settingsGet(path: String): String
86 | external fun settingsSet(path: String, value: String): Boolean
87 | external fun getState() : Int
88 | external fun kill()
89 | external fun resume()
90 | external fun openHomeMenu()
91 | external fun getTitleId(): String
92 | external fun supportsCustomDriverLoading() : Boolean
93 | external fun isInstallableFile(fd: Int) : Boolean
94 | external fun getDirInstallPath(sfoFd: Int) : String?
95 | // external fun forceMaxGpuClocks(enable : Boolean)
96 |
97 |
98 | companion object {
99 | var initialized = false
100 | val instance = RPCS3()
101 | var rootDirectory: String = ""
102 | var activeGame = mutableStateOf(null)
103 | var state = mutableStateOf(EmulatorState.Stopped)
104 |
105 | fun boot(path: String): BootResult {
106 | return BootResult.fromInt(instance.boot(path))
107 | }
108 |
109 | fun updateState() {
110 | val newState = EmulatorState.fromInt(instance.getState())
111 | if (newState != state.value) {
112 | state.value = newState
113 | }
114 | }
115 |
116 | fun getState(): EmulatorState {
117 | updateState()
118 | return state.value
119 | }
120 |
121 | init {
122 | System.loadLibrary("rpcs3-android")
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/RPCS3Theme.kt:
--------------------------------------------------------------------------------
1 |
2 | package net.rpcs3
3 |
4 | import androidx.compose.material3.*
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.foundation.isSystemInDarkTheme
7 | import android.app.Activity
8 | import androidx.compose.runtime.*
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.platform.LocalView
11 | import androidx.core.view.WindowCompat
12 | import androidx.core.view.WindowInsetsControllerCompat
13 |
14 | @Composable
15 | fun RPCS3Theme(
16 | darkTheme: Boolean = isSystemInDarkTheme(),
17 | content: @Composable () -> Unit
18 | ) {
19 | // TODO(Ishan09811): Implement dynamic colors option whenever settings gets implemented
20 | val colors = if (darkTheme) darkColorScheme() else lightColorScheme()
21 |
22 | val view = LocalView.current
23 | val activity = view.context as? Activity
24 |
25 | SideEffect {
26 | activity?.window?.apply {
27 | statusBarColor = android.graphics.Color.TRANSPARENT
28 | navigationBarColor = android.graphics.Color.TRANSPARENT
29 | isNavigationBarContrastEnforced = false
30 | val insetsController = WindowInsetsControllerCompat(this, decorView)
31 | insetsController.isAppearanceLightNavigationBars = !darkTheme
32 | insetsController.isAppearanceLightStatusBars = !darkTheme
33 | }
34 | }
35 |
36 | MaterialTheme(
37 | colorScheme = colors,
38 | typography = Typography(),
39 | content = content
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/UsbDeviceRepository.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3
2 |
3 | import android.app.Activity.USB_SERVICE
4 | import android.app.PendingIntent
5 | import android.content.BroadcastReceiver
6 | import android.content.Context
7 | import android.content.Intent
8 | import android.content.IntentFilter
9 | import android.hardware.usb.UsbDevice
10 | import android.hardware.usb.UsbDeviceConnection
11 | import android.hardware.usb.UsbManager
12 | import android.os.Build
13 | import android.util.Log
14 |
15 | private const val ACTION_USB_PERMISSION = "net.rpcs3.USB_PERMISSION"
16 |
17 | fun listenUsbEvents(context: Context): () -> Unit {
18 | val mPermissionIntent = PendingIntent.getBroadcast(
19 | context,
20 | 0,
21 | Intent(ACTION_USB_PERMISSION),
22 | PendingIntent.FLAG_MUTABLE or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
23 | PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
24 | } else {
25 | 0
26 | }
27 | )
28 | val usbManager = context.getSystemService(USB_SERVICE) as UsbManager
29 |
30 | val usbReceiver = object : BroadcastReceiver() {
31 | override fun onReceive(context: Context, intent: Intent) {
32 | if (UsbManager.ACTION_USB_DEVICE_DETACHED == intent.action) {
33 | Log.i("USB", "device detached")
34 | val device: UsbDevice? = with(intent) {
35 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
36 | getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
37 | else
38 | getParcelableExtra(UsbManager.EXTRA_DEVICE)
39 | }
40 | if (device != null) {
41 | UsbDeviceRepository.detach(device)
42 | }
43 |
44 | return
45 | }
46 |
47 | if (UsbManager.ACTION_USB_DEVICE_ATTACHED == intent.action) {
48 | val device: UsbDevice? = with(intent) {
49 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
50 | getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
51 | else
52 | getParcelableExtra(UsbManager.EXTRA_DEVICE)
53 | }
54 | Log.i("USB", "device attached")
55 | if (device != null) {
56 | if (usbManager.hasPermission(device)) {
57 | Log.i("USB", "permission granted, attaching")
58 | UsbDeviceRepository.attach(device, usbManager)
59 | } else {
60 | Log.i("USB", "no permission, requesting")
61 | usbManager.requestPermission(device, mPermissionIntent)
62 | }
63 | }
64 |
65 | return
66 | }
67 |
68 | if (ACTION_USB_PERMISSION == intent.action) {
69 | val device: UsbDevice? = with(intent) {
70 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
71 | getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
72 | else
73 | getParcelableExtra(UsbManager.EXTRA_DEVICE)
74 | }
75 |
76 | if (device != null && intent.getBooleanExtra(
77 | UsbManager.EXTRA_PERMISSION_GRANTED,
78 | false
79 | )
80 | ) {
81 | if (usbManager.hasPermission(device)) {
82 | Log.i("USB", "permission granted, attaching")
83 | UsbDeviceRepository.attach(device, usbManager)
84 | } else {
85 | Log.i("USB", "no permission after request")
86 | }
87 | } else {
88 | Log.i("USB", "permission request aborted")
89 | }
90 | }
91 | }
92 | }
93 |
94 | val filter = IntentFilter()
95 | filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
96 | filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
97 | filter.addAction(ACTION_USB_PERMISSION)
98 | context.registerReceiver(usbReceiver, filter, Context.RECEIVER_EXPORTED)
99 |
100 | for (usbDevice in usbManager.deviceList.values) {
101 | if (usbManager.hasPermission(usbDevice)) {
102 | UsbDeviceRepository.attach(usbDevice, usbManager)
103 | } else {
104 | usbManager.requestPermission(usbDevice, mPermissionIntent)
105 | }
106 | }
107 |
108 | return {
109 | context.unregisterReceiver(usbReceiver)
110 | }
111 | }
112 |
113 | class UsbDeviceRepository {
114 | companion object {
115 | private val devices = HashMap()
116 |
117 | fun attach(device: UsbDevice, usbManager: UsbManager) {
118 | if (devices[device] != null) {
119 | return
120 | }
121 |
122 | val connection = usbManager.openDevice(device)
123 | devices[device] = connection
124 | RPCS3.instance.usbDeviceEvent(
125 | connection.fileDescriptor,
126 | device.vendorId,
127 | device.productId,
128 | 0
129 | )
130 | }
131 |
132 | fun detach(device: UsbDevice) {
133 | val connection = devices[device]
134 | if (connection != null) {
135 | RPCS3.instance.usbDeviceEvent(connection.fileDescriptor, -1, -1, 1)
136 | connection.close()
137 |
138 | devices.remove(device)
139 | }
140 | }
141 | }
142 | }
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/dialogs/AlertDialogQueue.kt:
--------------------------------------------------------------------------------
1 |
2 | package net.rpcs3.dialogs
3 |
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.heightIn
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.width
12 | import androidx.compose.foundation.layout.height
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.foundation.rememberScrollState
15 | import androidx.compose.foundation.verticalScroll
16 | import androidx.compose.material3.BasicAlertDialog
17 | import androidx.compose.material3.Text
18 | import androidx.compose.material3.TextButton
19 | import androidx.compose.material3.Divider
20 | import androidx.compose.material3.MaterialTheme
21 | import androidx.compose.material3.Surface
22 | import androidx.compose.material3.ExperimentalMaterial3Api
23 | import androidx.compose.runtime.Composable
24 | import androidx.compose.runtime.mutableStateListOf
25 | import androidx.compose.runtime.remember
26 | import androidx.compose.runtime.derivedStateOf
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.unit.dp
29 |
30 | object AlertDialogQueue {
31 | val dialogs = mutableStateListOf()
32 |
33 | fun showDialog(
34 | title: String,
35 | message: String,
36 | onConfirm: () -> Unit = {},
37 | onDismiss: (() -> Unit)? = null,
38 | confirmText: String = "OK",
39 | dismissText: String = "Cancel"
40 | ) {
41 | dialogs.add(DialogData(title, message, onConfirm, onDismiss, confirmText, dismissText))
42 | }
43 |
44 | private fun dismissDialog() {
45 | if (dialogs.isNotEmpty()) {
46 | dialogs.removeAt(0)
47 | }
48 | }
49 |
50 | @OptIn(ExperimentalMaterial3Api::class)
51 | @Composable
52 | fun AlertDialog() {
53 | if (dialogs.isEmpty()) {
54 | return
55 | }
56 |
57 | val dialog = dialogs.first()
58 | val onDismiss = dialog.onDismiss
59 |
60 | val scrollState = rememberScrollState()
61 | val hasScrolled = remember { derivedStateOf { scrollState.value > 0 } }
62 |
63 | BasicAlertDialog(
64 | onDismissRequest = {
65 | onDismiss?.invoke()
66 | dismissDialog()
67 | },
68 | content = {
69 | Surface(
70 | shape = RoundedCornerShape(28.dp),
71 | tonalElevation = 6.dp,
72 | modifier = Modifier
73 | .fillMaxWidth()
74 | .padding(vertical = 16.dp)
75 | ) {
76 | Column(modifier = Modifier.padding(vertical = 16.dp)) {
77 | Text(
78 | dialog.title,
79 | modifier = Modifier.padding(horizontal = 16.dp),
80 | style = MaterialTheme.typography.headlineSmall,
81 | color = MaterialTheme.colorScheme.onSurface
82 | )
83 | Spacer(modifier = Modifier.height(8.dp))
84 |
85 | if (hasScrolled.value) {
86 | Divider()
87 | }
88 |
89 | Text(
90 | text = dialog.message,
91 | style = MaterialTheme.typography.bodyMedium,
92 | color = MaterialTheme.colorScheme.onSurfaceVariant,
93 | modifier = Modifier
94 | .heightIn(max = 200.dp)
95 | .verticalScroll(scrollState)
96 | .padding(vertical = 4.dp, horizontal = 16.dp)
97 | )
98 |
99 | if (hasScrolled.value) {
100 | Divider()
101 | }
102 |
103 | Spacer(modifier = Modifier.height(16.dp))
104 | Row(
105 | modifier = Modifier.fillMaxWidth(),
106 | horizontalArrangement = Arrangement.End
107 | ) {
108 | TextButton(
109 | onClick = {
110 | onDismiss?.invoke()
111 | dismissDialog()
112 | }
113 | ) {
114 | Text(text = dialog.dismissText)
115 | }
116 | Spacer(modifier = Modifier.width(8.dp))
117 | TextButton(
118 | onClick = {
119 | dialog.onConfirm()
120 | onDismiss?.invoke()
121 | dismissDialog()
122 | },
123 | modifier = Modifier.padding(end = 16.dp)
124 | ) {
125 | Text(text = dialog.confirmText)
126 | }
127 | }
128 | }
129 | }
130 | }
131 | )
132 | }
133 | }
134 |
135 | data class DialogData(
136 | val title: String,
137 | val message: String,
138 | val onConfirm: () -> Unit,
139 | val onDismiss: (() -> Unit)?,
140 | val confirmText: String,
141 | val dismissText: String
142 | )
143 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/overlay/PadOverlayButton.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3.overlay
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import android.content.res.Resources
6 | import android.graphics.Bitmap
7 | import android.graphics.drawable.BitmapDrawable
8 | import android.view.MotionEvent
9 | import kotlin.math.roundToInt
10 |
11 | class PadOverlayButton(private val context: Context, resources: Resources, image: Bitmap, private val digital1: Int, private val digital2: Int) : BitmapDrawable(resources, image) {
12 | private var pressed = false
13 | private var locked = -1
14 | private var origAlpha = alpha
15 | var dragging = false
16 | private var offsetX = 0
17 | private var offsetY = 0
18 | private var scaleFactor = 0.5f
19 | private var opacity = alpha
20 | var defaultSize: Pair = Pair(-1, -1)
21 | lateinit var defaultPosition: Pair
22 | private val prefs: SharedPreferences by lazy { context.getSharedPreferences("PadOverlayPrefs", Context.MODE_PRIVATE) }
23 |
24 | fun contains(x: Int, y: Int) = bounds.contains(x, y)
25 |
26 | fun onTouch(event: MotionEvent, pointerIndex: Int, padState: State): Boolean {
27 | val action = event.actionMasked
28 | var hit = false
29 | if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) {
30 | if (locked == -1) {
31 | locked = event.getPointerId(pointerIndex)
32 | pressed = true
33 | origAlpha = alpha
34 | alpha = 255
35 | hit = true
36 | }
37 | } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_CANCEL) {
38 | if (locked != -1 && (action == MotionEvent.ACTION_CANCEL || event.getPointerId(pointerIndex) == locked)) {
39 | pressed = false
40 | locked = -1
41 | alpha = origAlpha
42 | hit = true
43 | }
44 | }
45 |
46 | if (pressed) {
47 | padState.digital[0] = padState.digital[0] or digital1
48 | padState.digital[1] = padState.digital[1] or digital2
49 | } else {
50 | padState.digital[0] = padState.digital[0] and digital1.inv()
51 | padState.digital[1] = padState.digital[1] and digital2.inv()
52 | }
53 |
54 | return hit
55 | }
56 |
57 | fun startDragging(startX: Int, startY: Int) {
58 | dragging = true
59 | offsetX = startX - bounds.left
60 | offsetY = startY - bounds.top
61 | }
62 |
63 | fun updatePosition(x: Int, y: Int, force: Boolean = false) {
64 | if (dragging) {
65 | setBounds(x - offsetX, y - offsetY, x - offsetX + bounds.width(), y - offsetY + bounds.height())
66 | prefs.edit()
67 | .putInt("button_${digital1}_${digital2}_x", x - offsetX)
68 | .putInt("button_${digital1}_${digital2}_y", y - offsetY)
69 | .apply()
70 | } else if (force) {
71 | // don't use offsets as we aren't dragging
72 | setBounds(x, y, x + bounds.width(), y + bounds.height())
73 | prefs.edit()
74 | .putInt("button_${digital1}_${digital2}_x", x)
75 | .putInt("button_${digital1}_${digital2}_y", y)
76 | .apply()
77 | }
78 | }
79 |
80 | fun stopDragging() {
81 | dragging = false
82 | }
83 |
84 | fun setScale(percent: Int) {
85 | scaleFactor = percent / 100f
86 | val newWidth = (intrinsicWidth * scaleFactor).roundToInt()
87 | val newHeight = (intrinsicHeight * scaleFactor).roundToInt()
88 | setBounds(bounds.left, bounds.top, bounds.left + newWidth, bounds.top + newHeight)
89 | prefs.edit().putInt("button_${digital1}_${digital2}_scale", percent).apply()
90 | }
91 |
92 | fun setOpacity(percent: Int) {
93 | opacity = (255 * (percent / 100f)).roundToInt()
94 | alpha = opacity
95 | prefs.edit().putInt("button_${digital1}_${digital2}_opacity", percent).apply()
96 | }
97 |
98 | fun measureDefaultScale(): Int {
99 | if (defaultSize.second <= 0 || defaultSize.first <= 0) return 100
100 | val widthScale = defaultSize.second.toFloat() / intrinsicWidth * 100
101 | val heightScale = defaultSize.first.toFloat() / intrinsicHeight * 100
102 | return minOf(widthScale, heightScale).roundToInt()
103 | }
104 |
105 | fun resetConfigs() {
106 | setOpacity(50)
107 | setBounds(defaultPosition.first, defaultPosition.second, defaultPosition.first + defaultSize.second, defaultPosition.second + defaultSize.first)
108 | prefs.edit()
109 | .remove("button_${digital1}_${digital2}_scale")
110 | .remove("button_${digital1}_${digital2}_opacity")
111 | .remove("button_${digital1}_${digital2}_x")
112 | .remove("button_${digital1}_${digital2}_y")
113 | .apply()
114 | }
115 |
116 | fun getInfo(): Triple {
117 | return Triple("${digital1}_${digital2}", prefs.getInt("button_${digital1}_${digital2}_scale", measureDefaultScale()), prefs.getInt("button_${digital1}_${digital2}_opacity", 50))
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/overlay/PadOverlayStick.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3.overlay
2 |
3 | import android.content.res.Resources
4 | import android.graphics.Bitmap
5 | import android.graphics.Canvas
6 | import android.graphics.drawable.BitmapDrawable
7 | import android.view.MotionEvent
8 | import androidx.core.graphics.drawable.toDrawable
9 | import kotlin.math.atan2
10 | import kotlin.math.cos
11 | import kotlin.math.hypot
12 | import kotlin.math.sin
13 |
14 | class PadOverlayStick(
15 | resources: Resources,
16 | private val isLeft: Boolean,
17 | bg: Bitmap,
18 | stick: Bitmap,
19 | private val pressDigitalIndex: Int = 0,
20 | private val pressBit: Int = 0
21 | ) :
22 | BitmapDrawable(resources, stick) {
23 | private var bg = bg.toDrawable(resources)
24 | private var locked = -1
25 | private var pressX = -1
26 | private var pressY = -1
27 | private var bgOffsetX = 0
28 | private var bgOffsetY = 0
29 | fun contains(x: Int, y: Int) = bounds.contains(x, y)
30 |
31 | fun isActive(): Boolean {
32 | return locked != -1
33 | }
34 |
35 | fun onAdd(event: MotionEvent, pointerIndex: Int) {
36 | locked = event.getPointerId(pointerIndex)
37 | val x = event.getX(pointerIndex).toInt()
38 | val y = event.getY(pointerIndex).toInt()
39 |
40 | pressX = x
41 | pressY = y
42 |
43 | setBounds(
44 | x - bounds.width() / 2,
45 | y - bounds.height() / 2,
46 | x + bounds.width() / 2,
47 | y + bounds.height() / 2,
48 | )
49 | }
50 |
51 | fun onTouch(event: MotionEvent, pointerIndex: Int, padState: State): Int {
52 | val action = event.actionMasked
53 |
54 | if ((pressBit != 0 && (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN)) || (locked != -1 && action == MotionEvent.ACTION_MOVE)) {
55 | var activePointerIndex = pointerIndex
56 |
57 | if (action != MotionEvent.ACTION_MOVE) {
58 | if (locked == -1) {
59 | locked = event.getPointerId(pointerIndex)
60 | pressX = event.getX(pointerIndex).toInt()
61 | pressY = event.getY(pointerIndex).toInt()
62 | bgOffsetX = bg.bounds.centerX() - pressX
63 | bgOffsetY = bg.bounds.centerY() - pressY
64 |
65 | bg.setBounds(bg.bounds.left - bgOffsetX,bg.bounds.top - bgOffsetY, bg.bounds.right - bgOffsetX, bg.bounds.bottom - bgOffsetY)
66 | } else if (locked != event.getPointerId(pointerIndex)) {
67 | return 0
68 | }
69 | } else {
70 |
71 | for (i in 0.. bgR) {
99 | val L = atan2(y, x)
100 | x = bgR * cos(L)
101 | y = bgR * sin(L)
102 | }
103 |
104 | val stickX = ((x / bgR) * 127 + 128).toInt()
105 | val stickY = ((y / bgR) * 127 + 128).toInt()
106 |
107 | if (isLeft) {
108 | padState.leftStickX = stickX
109 | padState.leftStickY = stickY
110 | } else {
111 | padState.rightStickX = stickX
112 | padState.rightStickY = stickY
113 | }
114 |
115 | x += bgCenterX
116 | y += bgCenterY
117 |
118 | super.setBounds(
119 | x.toInt() - bounds.width() / 2,
120 | y.toInt() - bounds.height() / 2,
121 | x.toInt() + bounds.width() / 2,
122 | y.toInt() + bounds.height() / 2,
123 | )
124 |
125 | return 1
126 | }
127 |
128 | if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_CANCEL) {
129 | if (locked != -1 && (action == MotionEvent.ACTION_CANCEL || event.getPointerId(
130 | pointerIndex
131 | ) == locked)
132 | ) {
133 | locked = -1
134 |
135 | bg.setBounds(bg.bounds.left + bgOffsetX,bg.bounds.top + bgOffsetY, bg.bounds.right + bgOffsetX, bg.bounds.bottom + bgOffsetY)
136 | bgOffsetY = 0
137 | bgOffsetX = 0
138 |
139 | padState.digital[pressDigitalIndex] =
140 | padState.digital[pressDigitalIndex] and pressBit.inv()
141 |
142 | super.setBounds(bg.bounds)
143 |
144 | if (isLeft) {
145 | padState.leftStickX = 127
146 | padState.leftStickY = 127
147 | } else {
148 | padState.rightStickX = 127
149 | padState.rightStickY = 127
150 | }
151 | return -1
152 | }
153 | }
154 |
155 | return 0
156 | }
157 |
158 | override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
159 | super.setBounds(left, top, right, bottom)
160 | bg.setBounds(left, top, right, bottom)
161 | }
162 |
163 | override fun setAlpha(alpha: Int) {
164 | super.setAlpha(alpha)
165 | bg.alpha = alpha
166 | }
167 |
168 | override fun draw(canvas: Canvas) {
169 | bg.draw(canvas)
170 | super.draw(canvas)
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/provider/AppDataDocumentProvider.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3.provider
2 |
3 | import android.content.Context
4 | import android.database.Cursor
5 | import android.database.MatrixCursor
6 | import android.os.CancellationSignal
7 | import android.os.ParcelFileDescriptor
8 | import android.provider.DocumentsContract.Document
9 | import android.provider.DocumentsContract.Root
10 | import android.provider.DocumentsProvider
11 | import net.rpcs3.R
12 | import java.io.File
13 | import java.io.FileNotFoundException
14 | import java.io.IOException
15 |
16 | class AppDataDocumentProvider : DocumentsProvider() {
17 | companion object {
18 | const val ROOT_ID = "root"
19 | const val AUTHORITY = "net.rpcs3" + ".documents"
20 |
21 | private val DEFAULT_ROOT_PROJECTION = arrayOf(
22 | Root.COLUMN_ROOT_ID,
23 | Root.COLUMN_MIME_TYPES,
24 | Root.COLUMN_FLAGS,
25 | Root.COLUMN_ICON,
26 | Root.COLUMN_TITLE,
27 | Root.COLUMN_SUMMARY,
28 | Root.COLUMN_DOCUMENT_ID,
29 | Root.COLUMN_AVAILABLE_BYTES
30 | )
31 |
32 | private val DEFAULT_DOCUMENT_PROJECTION = arrayOf(
33 | Document.COLUMN_DOCUMENT_ID,
34 | Document.COLUMN_DISPLAY_NAME,
35 | Document.COLUMN_MIME_TYPE,
36 | Document.COLUMN_LAST_MODIFIED,
37 | Document.COLUMN_FLAGS,
38 | Document.COLUMN_SIZE
39 | )
40 | }
41 |
42 | private fun obtainDocumentId(file: File): String {
43 | val basePath = baseDirectory().absolutePath
44 | val fullPath = file.absolutePath
45 | return (ROOT_ID + "/" + fullPath.substring(basePath.length)).replace("//", "/")
46 | }
47 |
48 | private fun obtainFile(documentId: String): File {
49 | require(documentId.startsWith(ROOT_ID)) { "Invalid document id: $documentId" }
50 | return File(baseDirectory(), documentId.substring(ROOT_ID.length))
51 | }
52 |
53 | private fun context(): Context = context!!
54 |
55 | private fun baseDirectory(): File = context().getExternalFilesDir(null)!!
56 |
57 | override fun onCreate(): Boolean = true
58 |
59 | override fun queryRoots(projection: Array?): Cursor {
60 | val cursor = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION)
61 | cursor.newRow()
62 | .add(Root.COLUMN_ROOT_ID, ROOT_ID)
63 | .add(Root.COLUMN_SUMMARY, null)
64 | .add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_IS_CHILD or Root.FLAG_SUPPORTS_CREATE)
65 | .add(Root.COLUMN_DOCUMENT_ID, "$ROOT_ID/")
66 | .add(Root.COLUMN_AVAILABLE_BYTES, baseDirectory().freeSpace)
67 | .add(Root.COLUMN_TITLE, context().getString(R.string.app_name))
68 | .add(Root.COLUMN_MIME_TYPES, "*/*")
69 | .add(Root.COLUMN_ICON, R.mipmap.ic_rpcs3)
70 | return cursor
71 | }
72 |
73 | override fun queryDocument(documentId: String, projection: Array?): Cursor {
74 | val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
75 | includeFile(cursor, obtainFile(documentId))
76 | return cursor
77 | }
78 |
79 | private fun includeFile(cursor: MatrixCursor, file: File) {
80 | val flags = when {
81 | file.isDirectory -> Document.FLAG_DIR_SUPPORTS_CREATE or Document.FLAG_SUPPORTS_REMOVE or Document.FLAG_SUPPORTS_DELETE or Document.FLAG_SUPPORTS_RENAME
82 | else -> Document.FLAG_SUPPORTS_WRITE or Document.FLAG_SUPPORTS_REMOVE or Document.FLAG_SUPPORTS_DELETE or Document.FLAG_SUPPORTS_RENAME
83 | }
84 |
85 | cursor.newRow()
86 | .add(Document.COLUMN_DOCUMENT_ID, obtainDocumentId(file))
87 | .add(Document.COLUMN_MIME_TYPE, if (file.isDirectory) Document.MIME_TYPE_DIR else "application/octet-stream")
88 | .add(Document.COLUMN_FLAGS, flags)
89 | .add(Document.COLUMN_LAST_MODIFIED, file.lastModified())
90 | .add(Document.COLUMN_DISPLAY_NAME, file.name)
91 | .add(Document.COLUMN_SIZE, file.length())
92 | }
93 |
94 | override fun queryChildDocuments(parentDocumentId: String, projection: Array?, sortOrder: String?): Cursor {
95 | val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
96 | obtainFile(parentDocumentId).listFiles()?.forEach { includeFile(cursor, it) }
97 | return cursor
98 | }
99 |
100 | override fun createDocument(parentDocumentId: String, mimeType: String, displayName: String): String {
101 | val parent = obtainFile(parentDocumentId)
102 | val file = File(parent, displayName)
103 |
104 | if (!parent.exists()) throw FileNotFoundException("Parent doesn't exist")
105 |
106 | if (mimeType == Document.MIME_TYPE_DIR) {
107 | if (!file.mkdirs()) throw FileNotFoundException("Error while creating directory")
108 | } else {
109 | if (!file.createNewFile()) throw FileNotFoundException("Error while creating file")
110 | }
111 |
112 | return obtainDocumentId(file)
113 | }
114 |
115 | override fun deleteDocument(documentId: String) {
116 | val file = obtainFile(documentId)
117 | if (file.exists()) {
118 | file.deleteRecursively()
119 | } else {
120 | throw FileNotFoundException("File not exists")
121 | }
122 | }
123 |
124 | override fun openDocument(documentId: String, mode: String, signal: CancellationSignal?): ParcelFileDescriptor {
125 | return ParcelFileDescriptor.open(obtainFile(documentId), ParcelFileDescriptor.parseMode(mode))
126 | }
127 |
128 | override fun removeDocument(documentId: String, parentDocumentId: String) {
129 | deleteDocument(documentId)
130 | }
131 |
132 | override fun renameDocument(documentId: String?, displayName: String?): String {
133 | if (documentId == null || displayName == null) {
134 | throw IllegalArgumentException("Document ID and display name must not be null")
135 | }
136 |
137 | val file = obtainFile(documentId)
138 | if (!file.exists()) {
139 | throw FileNotFoundException("File not found: $documentId")
140 | }
141 |
142 | val parentDir = file.parentFile
143 | val newFile = File(parentDir, displayName)
144 |
145 | if (newFile.exists()) {
146 | throw FileAlreadyExistsException(newFile)
147 | }
148 |
149 | if (!file.renameTo(newFile)) {
150 | throw IOException("Failed to rename file: ${file.absolutePath} to ${newFile.absolutePath}")
151 | }
152 |
153 | return obtainDocumentId(newFile)
154 | }
155 |
156 | override fun isChildDocument(parentDocumentId: String, documentId: String): Boolean {
157 | return documentId.startsWith(parentDocumentId)
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/ui/common/Previews.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3.ui.common
2 |
3 | import androidx.compose.material3.Surface
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import net.rpcs3.RPCS3Theme
7 |
8 |
9 | /**
10 | * a composable function for previewing UI elements within the application's theme.
11 | *
12 | * this functions wraps the provided content within a Material3 `Surface` and applies the application's
13 | * theme for consistent M3 previews, otherwise, the preview defaults to using the system theme.
14 | *
15 | * @param modifier The modifier to be applied to the `Surface`.
16 | * @param content The content to be previewed.
17 | *
18 | * @see AppTheme
19 | */
20 |
21 | @Composable
22 | fun ComposePreview(
23 | modifier: Modifier = Modifier,
24 | content: @Composable () -> Unit
25 | ) {
26 | RPCS3Theme {
27 | Surface(
28 | modifier = modifier
29 | ) {
30 | content()
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/ui/settings/components/CompositionLocals.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3.ui.settings.components
2 |
3 | import androidx.compose.runtime.compositionLocalOf
4 |
5 |
6 | /**
7 | * Created using Android Studio
8 | * User: Muhammad Ashhal
9 | * Date: Wed, Mar 05, 2025
10 | * Time: 1:00 am
11 | */
12 |
13 | val LocalPreferenceState = compositionLocalOf { true }
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/ui/settings/components/base/BaseDialogPreference.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3.ui.settings.components.base
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.PaddingValues
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.size
11 | import androidx.compose.material3.AlertDialogDefaults
12 | import androidx.compose.material3.BasicAlertDialog
13 | import androidx.compose.material3.ExperimentalMaterial3Api
14 | import androidx.compose.material3.LocalContentColor
15 | import androidx.compose.material3.LocalTextStyle
16 | import androidx.compose.material3.MaterialTheme
17 | import androidx.compose.material3.Surface
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.runtime.CompositionLocalProvider
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.graphics.Color
23 | import androidx.compose.ui.graphics.Shape
24 | import androidx.compose.ui.text.TextStyle
25 | import androidx.compose.ui.unit.Dp
26 | import androidx.compose.ui.unit.dp
27 | import androidx.compose.ui.window.DialogProperties
28 |
29 | @OptIn(ExperimentalMaterial3Api::class)
30 | @Composable
31 | fun BaseDialogPreference(
32 | onDismissRequest: () -> Unit,
33 | modifier: Modifier = Modifier,
34 | icon: @Composable (() -> Unit)? = null,
35 | title: @Composable (() -> Unit)? = null,
36 | content: @Composable (() -> Unit)? = null,
37 | shape: Shape = AlertDialogDefaults.shape,
38 | containerColor: Color = AlertDialogDefaults.containerColor,
39 | iconContentColor: Color = AlertDialogDefaults.iconContentColor,
40 | titleContentColor: Color = AlertDialogDefaults.titleContentColor,
41 | contentColor: Color = AlertDialogDefaults.textContentColor,
42 | tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
43 | properties: DialogProperties = DialogProperties()
44 | ) = BasicAlertDialog(
45 | onDismissRequest = onDismissRequest,
46 | modifier = modifier,
47 | properties = properties
48 | ) {
49 | DialogContent(
50 | icon = icon,
51 | title = title,
52 | content = content,
53 | shape = shape,
54 | containerColor = containerColor,
55 | tonalElevation = tonalElevation,
56 | iconContentColor = iconContentColor,
57 | titleContentColor = titleContentColor,
58 | contentColor = contentColor
59 | )
60 | }
61 |
62 | @Composable
63 | private fun DialogContent(
64 | modifier: Modifier = Modifier,
65 | icon: (@Composable () -> Unit)?,
66 | title: (@Composable () -> Unit)?,
67 | content: @Composable (() -> Unit)?,
68 | shape: Shape,
69 | containerColor: Color,
70 | tonalElevation: Dp,
71 | iconContentColor: Color,
72 | titleContentColor: Color,
73 | contentColor: Color,
74 | ) {
75 | Surface(
76 | modifier = modifier,
77 | shape = shape,
78 | color = containerColor,
79 | tonalElevation = tonalElevation
80 | ) {
81 | Column(
82 | modifier = Modifier.padding(DialogPadding)
83 | ) {
84 | Row(
85 | modifier = Modifier.fillMaxWidth(),
86 | horizontalArrangement = Arrangement.Center,
87 | verticalAlignment = Alignment.CenterVertically
88 | ) {
89 | icon?.let {
90 | CompositionLocalProvider(value = LocalContentColor provides iconContentColor) {
91 | Box(
92 | modifier = Modifier
93 | .size(DialogIconSize)
94 | .align(Alignment.CenterVertically)
95 | ) {
96 | icon()
97 | }
98 | }
99 | }
100 | title?.let {
101 | ProvideContentColorTextStyle(
102 | contentColor = titleContentColor,
103 | textStyle = MaterialTheme.typography.titleLarge
104 | ) {
105 | Box(
106 | modifier = Modifier
107 | .padding(DialogTitlePadding)
108 | .align(Alignment.CenterVertically)
109 | ) { title() }
110 | }
111 | }
112 | }
113 |
114 |
115 | content?.let {
116 | val textStyle = MaterialTheme.typography.bodyLarge
117 | ProvideContentColorTextStyle(
118 | contentColor = contentColor,
119 | textStyle = textStyle
120 | ) {
121 | Box(
122 | Modifier
123 | .weight(weight = 1f, fill = false)
124 | .padding(DialogContentPadding)
125 | .align(Alignment.Start)
126 | ) {
127 | content()
128 | }
129 | }
130 | }
131 | }
132 | }
133 | }
134 |
135 |
136 | @Composable
137 | fun ProvideContentColorTextStyle(
138 | contentColor: Color,
139 | textStyle: TextStyle,
140 | content: @Composable () -> Unit
141 | ) {
142 | val mergedStyle = LocalTextStyle.current.merge(textStyle)
143 | CompositionLocalProvider(
144 | LocalContentColor provides contentColor,
145 | LocalTextStyle provides mergedStyle,
146 | content = content
147 | )
148 | }
149 |
150 |
151 | private val DialogPadding = PaddingValues(all = 16.dp)
152 | private val DialogIconSize = 36.dp
153 | private val DialogTitlePadding = PaddingValues(bottom = 8.dp)
154 | private val DialogContentPadding = PaddingValues(start = 8.dp, end = 8.dp, bottom = 12.dp)
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/ui/settings/components/base/BasePreference.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3.ui.settings.components.base
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.combinedClickable
9 | import androidx.compose.foundation.ExperimentalFoundationApi
10 | import androidx.compose.foundation.layout.heightIn
11 | import androidx.compose.foundation.shape.RoundedCornerShape
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
14 | import androidx.compose.material.icons.filled.Search
15 | import androidx.compose.material3.MaterialTheme
16 | import androidx.compose.material3.Surface
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.CompositionLocalProvider
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.graphics.Shape
22 | import androidx.compose.ui.tooling.preview.Preview
23 | import androidx.compose.ui.unit.Dp
24 | import androidx.compose.ui.unit.dp
25 | import net.rpcs3.ui.common.ComposePreview
26 | import net.rpcs3.ui.settings.components.LocalPreferenceState
27 | import net.rpcs3.ui.settings.components.core.PreferenceIcon
28 | import net.rpcs3.ui.settings.components.core.PreferenceSubtitle
29 | import net.rpcs3.ui.settings.components.core.PreferenceTitle
30 |
31 |
32 | /**
33 | * Created using Android Studio
34 | * User: Muhammad Ashhal
35 | * Date: Wed, Mar 05, 2025
36 | * Time: 1:03 am
37 | */
38 |
39 |
40 | /**
41 | * A composable function that creates a base layout for a preference item.
42 | *
43 | * @param title title of the preference.
44 | * @param modifier The modifier applied to the preference container.
45 | * @param subContent Optional composable content to display below the title.
46 | * @param leadingContent Optional composable content to display at the start of the preference item.
47 | * This is typically used for icons or other visual cues.
48 | *
49 | * @param trailingContent Optional composable content to display at the end of the preference item.
50 | * This is typically used for switches, checkboxes, or other interactive elements.
51 | *
52 | * @param shape The shape of the preference surface.
53 | * @param tonalElevation The tonal elevation of the preference surface.
54 | * @param shadowElevation The shadow elevation of the preference surface.
55 | * @param enabled Whether the preference is enabled or disabled.
56 | * @param onClick callback invoked when the preference item is clicked.
57 | *
58 | * @see Surface
59 | */
60 |
61 | @OptIn(ExperimentalFoundationApi::class)
62 | @Composable
63 | fun BasePreference(
64 | title: @Composable () -> Unit,
65 | modifier: Modifier = Modifier,
66 | subContent: @Composable (() -> Unit)? = null,
67 | value: @Composable (() -> Unit)? = null,
68 | leadingContent: @Composable (() -> Unit)? = null,
69 | trailingContent: @Composable (() -> Unit)? = null,
70 | shape: Shape = RoundedCornerShape(0),
71 | tonalElevation: Dp = 0.dp,
72 | shadowElevation: Dp = 0.dp,
73 | enabled: Boolean = true,
74 | onClick: () -> Unit,
75 | onLongClick: () -> Unit = {}
76 | ) {
77 | CompositionLocalProvider(
78 | LocalPreferenceState provides enabled
79 | ) {
80 | val preferenceOnClick: () -> Unit = {
81 | if (enabled) onClick()
82 | }
83 | Surface(
84 | modifier = modifier.combinedClickable(
85 | onClick = preferenceOnClick,
86 | onLongClick = onLongClick
87 | ),
88 | shape = shape,
89 | tonalElevation = tonalElevation,
90 | shadowElevation = shadowElevation
91 | ) {
92 | Row(
93 | modifier = Modifier
94 | .fillMaxWidth()
95 | .padding(horizontal = 16.dp)
96 | .heightIn(min = 72.dp),
97 | horizontalArrangement = Arrangement.spacedBy(16.dp),
98 | verticalAlignment = Alignment.CenterVertically
99 | ) {
100 | leadingContent?.invoke()
101 | Column(
102 | modifier = Modifier.weight(1f),
103 | verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically),
104 | ) {
105 | title()
106 | subContent?.invoke()
107 | value?.invoke()
108 | }
109 | trailingContent?.invoke()
110 | }
111 | }
112 | }
113 | }
114 |
115 | @Preview
116 | @Composable
117 | private fun BasePreferencePreview() {
118 | ComposePreview {
119 | BasePreference(
120 | title = { PreferenceTitle("Preference Title") },
121 | subContent = { PreferenceSubtitle("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ullamcorper tempor imperdiet. Tempor magna proident pariatur nonumy iusto, sint laborum possim accumsan, elit nonummy facer enim autem eiusmod lobortis reprehenderit molestie vel esse aliquyam cupiditat velit nisi aliquid ipsum. Erat accusam reprehenderit. Feugiat aliquyam iure. Nisi ex officia.", maxLines = 2) },
122 | leadingContent = { PreferenceIcon(Icons.Default.Search) },
123 | trailingContent = { PreferenceIcon(Icons.AutoMirrored.Default.KeyboardArrowRight) },
124 | onClick = {}
125 | )
126 | }
127 | }
128 |
129 | @Preview
130 | @Composable
131 | private fun BasePreferenceDisabledPreview() {
132 | ComposePreview {
133 | BasePreference(
134 | title = { PreferenceTitle("Preference Title") },
135 | subContent = { PreferenceSubtitle("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ullamcorper tempor imperdiet. Tempor magna proident pariatur nonumy iusto, sint laborum possim accumsan, elit nonummy facer enim autem eiusmod lobortis reprehenderit molestie vel esse aliquyam cupiditat velit nisi aliquid ipsum. Erat accusam reprehenderit. Feugiat aliquyam iure. Nisi ex officia.", maxLines = 2) },
136 | leadingContent = { PreferenceIcon(Icons.Default.Search) },
137 | trailingContent = { PreferenceIcon(Icons.AutoMirrored.Default.KeyboardArrowRight) },
138 | enabled = false,
139 | onClick = {}
140 | )
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/ui/settings/components/core/MaterialSwitch.kt:
--------------------------------------------------------------------------------
1 |
2 | package net.rpcs3.ui.settings.components.core
3 |
4 | import androidx.compose.animation.core.FastOutSlowInEasing
5 | import androidx.compose.animation.core.updateTransition
6 | import androidx.compose.animation.core.animateFloat
7 | import androidx.compose.animation.core.animateDp
8 | import androidx.compose.animation.animateColor
9 | import androidx.compose.animation.core.tween
10 | import androidx.compose.foundation.background
11 | import androidx.compose.foundation.clickable
12 | import androidx.compose.foundation.border
13 | import androidx.compose.foundation.layout.Box
14 | import androidx.compose.foundation.layout.offset
15 | import androidx.compose.foundation.layout.size
16 | import androidx.compose.foundation.layout.width
17 | import androidx.compose.foundation.layout.height
18 | import androidx.compose.foundation.shape.CircleShape
19 | import androidx.compose.foundation.shape.RoundedCornerShape
20 | import androidx.compose.material3.MaterialTheme
21 | import androidx.compose.material3.SwitchDefaults
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.runtime.getValue
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.draw.clip
26 | import androidx.compose.ui.graphics.Color
27 | import androidx.compose.ui.unit.dp
28 | import androidx.compose.ui.tooling.preview.Preview
29 | import androidx.compose.ui.Alignment
30 | import androidx.compose.ui.draw.shadow
31 |
32 | @Composable
33 | fun MaterialSwitch(
34 | checked: Boolean = true,
35 | enabled: Boolean = true,
36 | onCheckedChange: (Boolean) -> Unit,
37 | modifier: Modifier = Modifier
38 | ) {
39 | val colors = SwitchDefaults.colors()
40 | val transition = updateTransition(targetState = checked, label = "switchTransition")
41 |
42 | val thumbOffset by transition.animateFloat(
43 | transitionSpec = { tween(300, easing = FastOutSlowInEasing) },
44 | label = "thumbOffset"
45 | ) { if (it) 24f else 6f }
46 |
47 | val thumbSize by transition.animateDp(
48 | transitionSpec = { tween(300) },
49 | label = "thumbSize"
50 | ) { if (it) 24.dp else 15.dp }
51 |
52 | val trackColor by transition.animateColor(
53 | transitionSpec = { tween(300) },
54 | label = "trackColor"
55 | ) { if (it) if (enabled) colors.checkedTrackColor else colors.disabledCheckedTrackColor
56 | else if (enabled) colors.uncheckedTrackColor else colors.disabledUncheckedTrackColor
57 | }
58 |
59 | val thumbColor by transition.animateColor(
60 | transitionSpec = { tween(300) },
61 | label = "thumbColor"
62 | ) { if (it) if (enabled) colors.checkedThumbColor else colors.disabledCheckedThumbColor
63 | else if (enabled) colors.uncheckedThumbColor else colors.disabledUncheckedThumbColor
64 | }
65 |
66 | val borderColor by transition.animateColor(
67 | transitionSpec = { tween(300) },
68 | label = "borderColor"
69 | ) { if (it) if (enabled) colors.checkedBorderColor else colors.disabledCheckedBorderColor
70 | else if (enabled) colors.uncheckedBorderColor else colors.disabledUncheckedBorderColor
71 | }
72 |
73 | Box(
74 | modifier = modifier
75 | .width(52.dp)
76 | .height(32.dp)
77 | .clip(RoundedCornerShape(16.dp))
78 | .border(if (!checked) 2.dp else 0.dp, borderColor, RoundedCornerShape(16.dp))
79 | .background(trackColor)
80 | .clickable(enabled) { onCheckedChange(!checked) },
81 | contentAlignment = Alignment.CenterStart
82 | ) {
83 | Box(
84 | modifier = Modifier
85 | .size(thumbSize)
86 | .offset(x = thumbOffset.dp)
87 | .shadow(4.dp, CircleShape)
88 | .clip(CircleShape)
89 | .background(thumbColor)
90 | )
91 | }
92 | }
93 |
94 | @Preview
95 | @Composable
96 | fun MaterialSwitchPreview() {
97 | var switchState = true
98 | MaterialSwitch(
99 | checked = switchState,
100 | onCheckedChange = { switchState = it }
101 | )
102 | }
103 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/ui/settings/components/core/PreferenceIcon.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3.ui.settings.components.core
2 |
3 | import androidx.compose.material3.Icon
4 | import androidx.compose.material3.LocalContentColor
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.graphics.Color
8 | import androidx.compose.ui.graphics.painter.Painter
9 | import androidx.compose.ui.graphics.vector.ImageVector
10 | import androidx.compose.ui.unit.dp
11 | import net.rpcs3.ui.settings.components.LocalPreferenceState
12 | import net.rpcs3.ui.settings.util.preferenceColor
13 | import net.rpcs3.ui.settings.util.sizeIn
14 |
15 | /**
16 | * Created using Android Studio
17 | * User: Muhammad Ashhal
18 | * Date: Wed, Mar 05, 2025
19 | * Time: 1:32 am
20 | */
21 |
22 | @Composable
23 | fun PreferenceIcon(
24 | icon: ImageVector?,
25 | modifier: Modifier = Modifier,
26 | enabled: Boolean = LocalPreferenceState.current,
27 | contentDescription: String? = null,
28 | tint: Color = preferenceColor(enabled, LocalContentColor.current),
29 | ) {
30 | if (icon != null) {
31 | Icon(
32 | imageVector = icon,
33 | modifier = modifier.sizeIn(minSize = 24.dp, maxSize = 48.dp),
34 | contentDescription = contentDescription,
35 | tint = tint
36 | )
37 | }
38 | }
39 |
40 | @Composable
41 | fun PreferenceIcon(
42 | icon: Painter?,
43 | modifier: Modifier = Modifier,
44 | enabled: Boolean = LocalPreferenceState.current,
45 | contentDescription: String? = null,
46 | tint: Color = preferenceColor(enabled, LocalContentColor.current),
47 | ) {
48 | if (icon != null) {
49 | Icon(
50 | painter = icon,
51 | modifier = modifier.sizeIn(minSize = 24.dp, maxSize = 48.dp),
52 | contentDescription = contentDescription,
53 | tint = tint
54 | )
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/ui/settings/components/core/PreferenceTitle.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3.ui.settings.components.core
2 |
3 | import androidx.compose.material3.LocalContentColor
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.graphics.Color
9 | import androidx.compose.ui.text.AnnotatedString
10 | import androidx.compose.ui.text.TextStyle
11 | import androidx.compose.ui.text.font.FontWeight
12 | import androidx.compose.ui.text.style.TextOverflow
13 | import androidx.compose.ui.unit.sp
14 | import net.rpcs3.ui.settings.components.LocalPreferenceState
15 | import net.rpcs3.ui.settings.util.preferenceColor
16 | import net.rpcs3.ui.settings.util.preferenceSubtitleColor
17 |
18 | /**
19 | * Created using Android Studio
20 | * User: Muhammad Ashhal
21 | * Date: Wed, Mar 05, 2025
22 | * Time: 1:32 am
23 | */
24 |
25 | @Composable
26 | internal fun PreferenceTitle(
27 | title: String,
28 | modifier: Modifier = Modifier,
29 | enabled: Boolean = LocalPreferenceState.current,
30 | maxLines: Int = 1,
31 | overflow: TextOverflow = TextOverflow.Ellipsis,
32 | style: TextStyle = MaterialTheme.typography.headlineMedium.copy(
33 | fontSize = 17.sp,
34 | fontWeight = FontWeight.Normal,
35 | lineHeight = 22.sp
36 | ),
37 | color: Color = preferenceColor(enabled, LocalContentColor.current)
38 | ) {
39 | Text(
40 | text = title,
41 | modifier = modifier,
42 | style = style,
43 | maxLines = maxLines,
44 | overflow = overflow,
45 | color = color,
46 | )
47 | }
48 |
49 | @Composable
50 | internal fun PreferenceValue(
51 | text: String,
52 | modifier: Modifier = Modifier,
53 | enabled: Boolean = LocalPreferenceState.current,
54 | maxLines: Int = 1,
55 | overflow: TextOverflow = TextOverflow.Ellipsis,
56 | style: TextStyle = MaterialTheme.typography.labelMedium.copy(
57 | fontSize = 13.sp,
58 | fontWeight = FontWeight.Bold,
59 | ),
60 | color: Color = preferenceColor(enabled, LocalContentColor.current)
61 | ) {
62 | Text(
63 | text = text,
64 | modifier = modifier,
65 | style = style,
66 | maxLines = maxLines,
67 | overflow = overflow,
68 | color = color,
69 | )
70 | }
71 |
72 | @Composable
73 | internal fun PreferenceTitle(
74 | title: AnnotatedString,
75 | modifier: Modifier = Modifier,
76 | enabled: Boolean = LocalPreferenceState.current,
77 | maxLines: Int = 1,
78 | overflow: TextOverflow = TextOverflow.Ellipsis,
79 | style: TextStyle = MaterialTheme.typography.titleMedium,
80 | color: Color = preferenceColor(enabled, LocalContentColor.current)
81 | ) {
82 | Text(
83 | text = title,
84 | modifier = modifier,
85 | style = style,
86 | maxLines = maxLines,
87 | overflow = overflow,
88 | color = color,
89 | )
90 | }
91 |
92 | @Composable
93 | fun PreferenceSubtitle(
94 | text: String,
95 | modifier: Modifier = Modifier,
96 | enabled: Boolean = LocalPreferenceState.current,
97 | maxLines: Int = 2,
98 | overflow: TextOverflow = TextOverflow.Ellipsis,
99 | style: TextStyle = MaterialTheme.typography.bodyMedium,
100 | color: Color = preferenceSubtitleColor(enabled, LocalContentColor.current),
101 | ) {
102 | Text(
103 | text = text,
104 | modifier = modifier,
105 | style = style,
106 | maxLines = maxLines,
107 | overflow = overflow,
108 | color = color,
109 | )
110 | }
111 |
112 | @Composable
113 | fun PreferenceSubtitle(
114 | text: AnnotatedString,
115 | modifier: Modifier = Modifier,
116 | enabled: Boolean = LocalPreferenceState.current,
117 | maxLines: Int = 2,
118 | overflow: TextOverflow = TextOverflow.Ellipsis,
119 | style: TextStyle = MaterialTheme.typography.bodyMedium,
120 | color: Color = preferenceSubtitleColor(enabled, LocalContentColor.current),
121 | ) {
122 | Text(
123 | text = text,
124 | modifier = modifier,
125 | style = style,
126 | maxLines = maxLines,
127 | overflow = overflow,
128 | color = color,
129 | )
130 | }
131 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/ui/settings/components/preference/CheckboxPreference.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3.ui.settings.components.preference
2 |
3 | import androidx.compose.material.icons.Icons
4 | import androidx.compose.material.icons.filled.Build
5 | import androidx.compose.material3.Checkbox
6 | import androidx.compose.material3.CheckboxColors
7 | import androidx.compose.material3.CheckboxDefaults
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.runtime.setValue
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.vector.ImageVector
15 | import androidx.compose.ui.tooling.preview.PreviewLightDark
16 | import net.rpcs3.ui.common.ComposePreview
17 | import net.rpcs3.ui.settings.components.core.PreferenceIcon
18 | import net.rpcs3.ui.settings.components.core.PreferenceSubtitle
19 | import net.rpcs3.ui.settings.components.core.PreferenceTitle
20 |
21 |
22 | /**
23 | * Created using Android Studio
24 | * User: Muhammad Ashhal
25 | * Date: Sat, Mar 08, 2025
26 | * Time: 7:26 pm
27 | */
28 |
29 | @Composable
30 | fun CheckboxPreference(
31 | checked: Boolean,
32 | title: @Composable () -> Unit,
33 | leadingIcon: @Composable () -> Unit,
34 | modifier: Modifier = Modifier,
35 | subtitle: @Composable (() -> Unit)? = null,
36 | enabled: Boolean = true,
37 | checkboxColors: CheckboxColors = CheckboxDefaults.colors(),
38 | onClick: (Boolean) -> Unit
39 | ) {
40 | val onValueUpdated: (Boolean) -> Unit = { newValue -> onClick(newValue) }
41 | RegularPreference(
42 | title = title,
43 | leadingIcon = leadingIcon,
44 | modifier = modifier,
45 | subtitle = subtitle,
46 | enabled = enabled,
47 | trailingContent = {
48 | Checkbox(
49 | checked = checked,
50 | onCheckedChange = { onValueUpdated(it) },
51 | enabled = enabled,
52 | colors = checkboxColors,
53 | )
54 | },
55 | onClick = { onValueUpdated(!checked) }
56 | )
57 | }
58 |
59 | @Composable
60 | fun CheckboxPreference(
61 | checked: Boolean,
62 | title: String,
63 | leadingIcon: ImageVector,
64 | modifier: Modifier = Modifier,
65 | subtitle: @Composable (() -> Unit)? = null,
66 | enabled: Boolean = true,
67 | checkboxColors: CheckboxColors = CheckboxDefaults.colors(),
68 | onClick: (Boolean) -> Unit
69 | ) {
70 | CheckboxPreference(
71 | checked = checked,
72 | title = { PreferenceTitle(title = title) },
73 | leadingIcon = { PreferenceIcon(icon = leadingIcon) },
74 | modifier = modifier,
75 | subtitle = subtitle,
76 | enabled = enabled,
77 | checkboxColors = checkboxColors,
78 | onClick = onClick
79 | )
80 | }
81 |
82 | @PreviewLightDark
83 | @Composable
84 | private fun CheckboxPreferencePreview() {
85 | ComposePreview {
86 | var isChecked by remember { mutableStateOf(true) }
87 | CheckboxPreference(
88 | checked = isChecked,
89 | title = "Enable Something",
90 | subtitle = { PreferenceSubtitle(text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.") },
91 | leadingIcon = Icons.Default.Build
92 | ) { isChecked = it }
93 | }
94 | }
95 |
96 | @PreviewLightDark
97 | @Composable
98 | private fun CheckboxPreferenceDisabledPreview() {
99 | ComposePreview {
100 | var isChecked by remember { mutableStateOf(false) }
101 | CheckboxPreference(
102 | checked = isChecked,
103 | title = "Enable Something",
104 | subtitle = { PreferenceSubtitle(text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.") },
105 | leadingIcon = Icons.Default.Build,
106 | enabled = false
107 | ) { isChecked = it }
108 | }
109 | }
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/ui/settings/components/preference/HomePreference.kt:
--------------------------------------------------------------------------------
1 |
2 | package net.rpcs3.ui.settings.components.preference
3 |
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.material3.*
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.graphics.vector.ImageVector
11 | import androidx.compose.ui.unit.dp
12 | import androidx.compose.ui.unit.sp
13 | import androidx.compose.ui.text.font.FontWeight
14 |
15 | @Composable
16 | fun HomePreference(
17 | icon: @Composable (() -> Unit) = {},
18 | title: String,
19 | description: String,
20 | onClick: () -> Unit
21 | ) {
22 | Card(
23 | modifier = Modifier
24 | .fillMaxWidth()
25 | .padding(horizontal = 12.dp)
26 | .padding(bottom = 24.dp)
27 | .clickable { onClick() },
28 | elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
29 | shape = MaterialTheme.shapes.medium
30 | ) {
31 | Row(
32 | modifier = Modifier
33 | .fillMaxWidth()
34 | .padding(vertical = 10.dp, horizontal = 20.dp),
35 | verticalAlignment = Alignment.CenterVertically
36 | ) {
37 | icon.invoke()
38 |
39 | Column(
40 | modifier = Modifier
41 | .weight(1f)
42 | .padding(start = 16.dp)
43 | ) {
44 | Text(
45 | text = title,
46 | style = MaterialTheme.typography.bodyMedium,
47 | fontWeight = FontWeight.Bold,
48 | fontSize = 16.sp
49 | )
50 |
51 | Text(
52 | text = description,
53 | style = MaterialTheme.typography.bodySmall,
54 | fontSize = 14.sp,
55 | modifier = Modifier.padding(top = 5.dp)
56 | )
57 | }
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/ui/settings/components/preference/RegularPreference.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3.ui.settings.components.preference
2 |
3 | import androidx.compose.material.icons.Icons
4 | import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
5 | import androidx.compose.material.icons.filled.Settings
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.graphics.vector.ImageVector
9 | import androidx.compose.ui.tooling.preview.PreviewLightDark
10 | import net.rpcs3.ui.common.ComposePreview
11 | import net.rpcs3.ui.settings.components.base.BasePreference
12 | import net.rpcs3.ui.settings.components.core.PreferenceIcon
13 | import net.rpcs3.ui.settings.components.core.PreferenceSubtitle
14 | import net.rpcs3.ui.settings.components.core.PreferenceTitle
15 |
16 |
17 | /**
18 | * Created using Android Studio
19 | * User: Muhammad Ashhal
20 | * Date: Wed, Mar 05, 2025
21 | * Time: 1:01 am
22 | */
23 |
24 | /**
25 | * A regular preference item.
26 | * This is a simple preference item with a title, subtitle, leading icon, and trailing content.
27 | * This can also be called a simple TextPreference.
28 | * which is just a preference item, meant to show something to the user.
29 | * or be used to navigate the user to another screen.
30 | */
31 |
32 | @Composable
33 | fun RegularPreference(
34 | title: @Composable () -> Unit,
35 | leadingIcon: @Composable (() -> Unit) = {},
36 | modifier: Modifier = Modifier,
37 | subtitle: @Composable (() -> Unit)? = null,
38 | value: @Composable (() -> Unit)? = null,
39 | trailingContent: @Composable (() -> Unit)? = null,
40 | enabled: Boolean = true,
41 | onClick: () -> Unit,
42 | onLongClick: () -> Unit = {}
43 | ) {
44 | BasePreference(
45 | title = title,
46 | modifier = modifier,
47 | subContent = subtitle,
48 | value = value,
49 | leadingContent = leadingIcon,
50 | trailingContent = trailingContent,
51 | enabled = enabled,
52 | onClick = onClick,
53 | onLongClick = onLongClick
54 | )
55 | }
56 |
57 | @Composable
58 | fun RegularPreference(
59 | title: String,
60 | leadingIcon: ImageVector? = null,
61 | modifier: Modifier = Modifier,
62 | subtitle: @Composable (() -> Unit)? = null,
63 | value: @Composable (() -> Unit)? = null,
64 | trailingContent: @Composable (() -> Unit)? = null,
65 | enabled: Boolean = true,
66 | onClick: () -> Unit,
67 | onLongClick: () -> Unit = {}
68 | ) {
69 | RegularPreference(
70 | title = { PreferenceTitle(title = title) },
71 | leadingIcon = { PreferenceIcon(icon = leadingIcon) },
72 | modifier = modifier,
73 | subtitle = subtitle,
74 | value = value,
75 | trailingContent = trailingContent,
76 | enabled = enabled,
77 | onClick = onClick,
78 | onLongClick = onLongClick
79 | )
80 | }
81 |
82 | @PreviewLightDark
83 | @Composable
84 | private fun RegularPreferencePreview() {
85 | ComposePreview {
86 | RegularPreference(
87 | title = "Install Firmware",
88 | leadingIcon = Icons.Default.Settings,
89 | subtitle = { PreferenceSubtitle(text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ullamcorper tempor imperdiet. Tempor magna proident pariatur nonumy iusto, sint laborum possim accumsan, elit nonummy facer enim autem eiusmod lobortis reprehenderit molestie vel esse aliquyam cupiditat velit nisi aliquid ipsum. Erat accusam reprehenderit. Feugiat aliquyam iure. Nisi ex officia.") },
90 | trailingContent = { PreferenceIcon(icon = Icons.AutoMirrored.Default.KeyboardArrowRight) },
91 | onClick = { }
92 | )
93 | }
94 | }
95 |
96 | @PreviewLightDark
97 | @Composable
98 | private fun RegularPreferenceDisabledPreview() {
99 | ComposePreview {
100 | RegularPreference(
101 | title = "Advanced Settings",
102 | leadingIcon = Icons.Default.Settings,
103 | subtitle = { PreferenceSubtitle(text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ullamcorper tempor imperdiet. Tempor magna proident pariatur nonumy iusto, sint laborum possim accumsan, elit nonummy facer enim autem eiusmod lobortis reprehenderit molestie vel esse aliquyam cupiditat velit nisi aliquid ipsum. Erat accusam reprehenderit. Feugiat aliquyam iure. Nisi ex officia.") },
104 | trailingContent = { PreferenceIcon(icon = Icons.AutoMirrored.Default.KeyboardArrowRight) },
105 | enabled = false,
106 | onClick = { }
107 | )
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/ui/settings/components/preference/SliderPreference.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3.ui.settings.components.preference
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.text.KeyboardOptions
6 | import androidx.compose.material.icons.Icons
7 | import androidx.compose.material.icons.filled.Refresh
8 | import androidx.compose.material3.AlertDialog
9 | import androidx.compose.material3.OutlinedTextField
10 | import androidx.compose.material3.Slider
11 | import androidx.compose.material3.SliderColors
12 | import androidx.compose.material3.SliderDefaults
13 | import androidx.compose.material3.Text
14 | import androidx.compose.material3.TextButton
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.getValue
17 | import androidx.compose.runtime.mutableFloatStateOf
18 | import androidx.compose.runtime.mutableStateOf
19 | import androidx.compose.runtime.remember
20 | import androidx.compose.runtime.setValue
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.graphics.vector.ImageVector
23 | import androidx.compose.ui.text.input.KeyboardType
24 | import androidx.compose.ui.tooling.preview.PreviewLightDark
25 | import androidx.compose.ui.unit.dp
26 | import net.rpcs3.ui.common.ComposePreview
27 | import net.rpcs3.ui.settings.components.core.PreferenceIcon
28 | import net.rpcs3.ui.settings.components.core.PreferenceSubtitle
29 | import net.rpcs3.ui.settings.components.core.PreferenceTitle
30 |
31 |
32 | /**
33 | * Created using Android Studio
34 | * User: Muhammad Ashhal
35 | * Date: Sat, Mar 08, 2025
36 | * Time: 10:35 pm
37 | */
38 |
39 | @Composable
40 | fun SliderPreference(
41 | value: Float,
42 | onValueChange: (Float) -> Unit,
43 | title: String,
44 | modifier: Modifier = Modifier,
45 | leadingIcon: ImageVector? = null,
46 | subtitle: String? = null,
47 | enabled: Boolean = true,
48 | valueRange: ClosedFloatingPointRange = 0f..1f,
49 | steps: Int = 0,
50 | valueContent: @Composable (() -> Unit)? = null,
51 | sliderColors: SliderColors = SliderDefaults.colors(),
52 | onLongClick: () -> Unit = {}
53 | ) {
54 | var showDialog by remember { mutableStateOf(false) }
55 | var tempValue by remember { mutableFloatStateOf(value) }
56 | var textValue by remember { mutableStateOf(value.toInt().toString()) }
57 | var isError by remember { mutableStateOf(false) }
58 |
59 | val stepSize = 1
60 |
61 | fun isStepNotAligned(input: Float): Boolean {
62 | return input % stepSize != 0f
63 | }
64 |
65 | RegularPreference(
66 | modifier = modifier,
67 | title = { PreferenceTitle(title = title) },
68 | leadingIcon = { PreferenceIcon(icon = leadingIcon) },
69 | subtitle = { subtitle?.let { PreferenceSubtitle(text = it) } },
70 | enabled = enabled,
71 | onClick = { showDialog = true },
72 | value = valueContent,
73 | onLongClick = onLongClick
74 | )
75 |
76 | if (showDialog) {
77 | AlertDialog(
78 | onDismissRequest = { showDialog = false },
79 | title = { Text(title) },
80 | text = {
81 | Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
82 | OutlinedTextField(
83 | value = textValue,
84 | onValueChange = { input ->
85 | textValue = input
86 | val parsedValue = input.toFloatOrNull()
87 | if (parsedValue != null && parsedValue in valueRange) {
88 | isError = isStepNotAligned(parsedValue)
89 | if (!isError) tempValue = parsedValue
90 | } else {
91 | isError = true
92 | }
93 | },
94 | keyboardOptions = KeyboardOptions.Default.copy(
95 | keyboardType = KeyboardType.Number
96 | ),
97 | isError = isError,
98 | label = { Text("Value") },
99 | supportingText = {
100 | if (isError) {
101 | Text("Value must be a multiple of step size $stepSize within ${valueRange.start} to ${valueRange.endInclusive}")
102 | }
103 | }
104 | )
105 |
106 | if ((valueRange.endInclusive - valueRange.start) < 1000) {
107 | Slider(
108 | value = tempValue,
109 | onValueChange = { newValue ->
110 | tempValue = newValue
111 | textValue = newValue.toInt().toString()
112 | },
113 | valueRange = valueRange,
114 | steps = steps,
115 | colors = sliderColors
116 | )
117 | }
118 | }
119 | },
120 | confirmButton = {
121 | TextButton(
122 | onClick = {
123 | if (!isError) {
124 | onValueChange(tempValue)
125 | showDialog = false
126 | }
127 | },
128 | enabled = !isError
129 | ) {
130 | Text("OK")
131 | }
132 | },
133 | dismissButton = {
134 | TextButton(onClick = {
135 | showDialog = false
136 | tempValue = value
137 | textValue = value.toInt().toString()
138 | }) {
139 | Text("Cancel")
140 | }
141 | }
142 | )
143 | }
144 | }
145 |
146 | @PreviewLightDark
147 | @Composable
148 | private fun SliderPreferencePreview() {
149 | ComposePreview {
150 | var value by remember { mutableFloatStateOf(0.5f) }
151 | SliderPreference(
152 | value = value,
153 | onValueChange = { value = it },
154 | title = "Refresh Duration",
155 | leadingIcon = Icons.Default.Refresh,
156 | subtitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit."
157 | )
158 | }
159 | }
160 |
161 | @PreviewLightDark
162 | @Composable
163 | private fun SliderPreferenceDisabledPreview() {
164 | ComposePreview {
165 | var value by remember { mutableFloatStateOf(0.5f) }
166 | SliderPreference(
167 | value = value,
168 | onValueChange = { value = it },
169 | title = "Refresh Duration",
170 | leadingIcon = Icons.Default.Refresh,
171 | subtitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
172 | enabled = false
173 | )
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/ui/settings/components/preference/SwitchPreference.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3.ui.settings.components.preference
2 |
3 | import androidx.compose.material.icons.Icons
4 | import androidx.compose.material.icons.filled.Build
5 | import net.rpcs3.ui.settings.components.core.MaterialSwitch
6 | import androidx.compose.material3.SwitchColors
7 | import androidx.compose.material3.SwitchDefaults
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.getValue
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.compose.runtime.remember
12 | import androidx.compose.runtime.setValue
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.vector.ImageVector
15 | import androidx.compose.ui.tooling.preview.Preview
16 | import androidx.compose.ui.tooling.preview.PreviewLightDark
17 | import net.rpcs3.ui.common.ComposePreview
18 | import net.rpcs3.ui.settings.components.core.PreferenceIcon
19 | import net.rpcs3.ui.settings.components.core.PreferenceSubtitle
20 | import net.rpcs3.ui.settings.components.core.PreferenceTitle
21 |
22 | /**
23 | * Created using Android Studio
24 | * User: Muhammad Ashhal
25 | * Date: Sat, Mar 08, 2025
26 | * Time: 6:59 pm
27 | */
28 |
29 | @Composable
30 | fun SwitchPreference(
31 | checked: Boolean,
32 | title: @Composable () -> Unit,
33 | leadingIcon: @Composable () -> Unit = {},
34 | modifier: Modifier = Modifier,
35 | subtitle: @Composable (() -> Unit)? = null,
36 | enabled: Boolean = true,
37 | switchColors: SwitchColors = SwitchDefaults.colors(),
38 | onClick: (Boolean) -> Unit,
39 | onLongClick: () -> Unit = {}
40 | ) {
41 | val onValueUpdated: (Boolean) -> Unit = { newValue -> onClick(newValue) }
42 | RegularPreference(
43 | title = title,
44 | subtitle = subtitle,
45 | modifier = modifier,
46 | leadingIcon = leadingIcon,
47 | trailingContent = {
48 | MaterialSwitch(
49 | checked = checked,
50 | onCheckedChange = { onValueUpdated(it) },
51 | enabled = enabled
52 | )
53 | },
54 | enabled = enabled,
55 | onClick = { onValueUpdated(!checked) },
56 | onLongClick = onLongClick
57 | )
58 | }
59 |
60 | @Composable
61 | fun SwitchPreference(
62 | checked: Boolean,
63 | title: String,
64 | leadingIcon: ImageVector? = null,
65 | modifier: Modifier = Modifier,
66 | subtitle: @Composable (() -> Unit)? = null,
67 | enabled: Boolean = true,
68 | switchColors: SwitchColors = SwitchDefaults.colors(),
69 | onClick: (Boolean) -> Unit,
70 | onLongClick: () -> Unit = {}
71 | ) {
72 | SwitchPreference(
73 | checked = checked,
74 | title = { PreferenceTitle(title = title) },
75 | leadingIcon = { PreferenceIcon(icon = leadingIcon) },
76 | modifier = modifier,
77 | subtitle = subtitle,
78 | enabled = enabled,
79 | switchColors = switchColors,
80 | onClick = onClick,
81 | onLongClick = onLongClick
82 | )
83 | }
84 |
85 | @PreviewLightDark
86 | @Composable
87 | private fun SwitchPreview() {
88 | ComposePreview {
89 | var switchState by remember { mutableStateOf(true) }
90 | SwitchPreference(
91 | checked = switchState,
92 | title = "Enable Something",
93 | subtitle = { PreferenceSubtitle(text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.") },
94 | leadingIcon = Icons.Default.Build,
95 | onClick = {
96 | switchState = it
97 | }
98 | )
99 | }
100 | }
101 |
102 | @PreviewLightDark
103 | @Composable
104 | private fun SwitchDisabledPreview() {
105 | ComposePreview {
106 | var switchState by remember { mutableStateOf(true) }
107 | SwitchPreference(
108 | checked = switchState,
109 | title = "Enable Something",
110 | subtitle = { PreferenceSubtitle(text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.") },
111 | leadingIcon = Icons.Default.Build,
112 | enabled = false,
113 | onClick = {
114 | switchState = it
115 | }
116 | )
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/ui/settings/util/ModifierExt.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3.ui.settings.util
2 |
3 | import androidx.compose.foundation.layout.sizeIn
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.unit.Dp
6 |
7 | /**
8 | * Created using Android Studio
9 | * User: Muhammad Ashhal
10 | * Date: Wed, Mar 05, 2025
11 | * Time: 1:34 am
12 | */
13 |
14 | /**
15 | * Constrains the size of the element to be within given bounds.
16 | *
17 | * The element's size will be at least [minSize] and at most [maxSize] in both dimensions.
18 | * If only one constraint is specified, the other dimension will be determined by the content.
19 | *
20 | * @param minSize The minimum size of the layout element in both dimensions.
21 | * @param maxSize The maximum size of the layout element in both dimensions.
22 | */
23 | fun Modifier.sizeIn(
24 | minSize: Dp = Dp.Unspecified,
25 | maxSize: Dp = Dp.Unspecified
26 | ) = this.sizeIn(minWidth = minSize, minHeight = minSize, maxWidth = maxSize, maxHeight = maxSize)
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/ui/settings/util/Theming.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3.ui.settings.util
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | /**
6 | * Created using Android Studio
7 | * User: Muhammad Ashhal
8 | * Date: Wed, Mar 05, 2025
9 | * Time: 1:34 am
10 | */
11 |
12 | /**
13 | * A low level of alpha used to represent disabled components, such as text in a disabled Button.
14 | */
15 | internal const val DisabledAlpha = 0.38f
16 |
17 | internal const val MediumAlpha = 0.67f
18 |
19 | /**
20 | * Returns a color to be used for preference title or leading/trailing icons.
21 | *
22 | * If the preference is enabled, the provided `contentColor` is returned as is.
23 | * If the preference is disabled, the `contentColor` is returned with its alpha
24 | * adjusted to represent a disabled state.
25 | *
26 | * @param enabled Whether the preference is enabled.
27 | * @param contentColor The base color to be used for the preference.
28 | * @return The color to be used for the preference title or icons.
29 | */
30 | fun preferenceColor(enabled: Boolean, contentColor: Color) =
31 | if (!enabled) contentColor.copy(alpha = DisabledAlpha) else contentColor
32 |
33 | /**
34 | * Returns a color to be used for preference subtitle.
35 | *
36 | * The returned color is based on the enabled state of the preference and the provided content color.
37 | * If the preference is disabled, the content color will be modified to have a lower alpha value
38 | * representing the disabled state. Otherwise, the content color will be modified to have a medium
39 | * alpha value.
40 | *
41 | * @param enabled Whether the preference is enabled.
42 | * @param contentColor The base content color of the preference subtitle.
43 | * @return The color to be used for the preference subtitle.
44 | */
45 | fun preferenceSubtitleColor(enabled: Boolean, contentColor: Color) =
46 | if (!enabled) contentColor.copy(alpha = DisabledAlpha) else contentColor.copy(alpha = MediumAlpha)
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/utils/DriverPackageMetadata.kt:
--------------------------------------------------------------------------------
1 |
2 | package net.rpcs3.utils
3 |
4 | import kotlinx.serialization.Serializable
5 | import kotlinx.serialization.SerializationException
6 | import kotlinx.serialization.json.Json
7 | import kotlinx.serialization.json.decodeFromJsonElement
8 | import kotlinx.serialization.json.intOrNull
9 | import kotlinx.serialization.json.jsonObject
10 | import kotlinx.serialization.json.jsonPrimitive
11 | import java.io.File
12 |
13 | /**
14 | @url: https://github.com/strato-emu/strato/blob/ae1566a48285816a87e81d4aeb40bd2f4e56e60b/app/src/main/java/org/stratoemu/strato/data/DriverPackageMetadata.kt#L13
15 | */
16 | data class GpuDriverMetadata(
17 | val name : String,
18 | val author : String,
19 | val packageVersion : String,
20 | val vendor : String,
21 | val driverVersion : String,
22 | val minApi : Int,
23 | val description : String,
24 | val libraryName : String,
25 | ) {
26 | private constructor(metadataV1 : GpuDriverMetadataV1) : this(
27 | name = metadataV1.name,
28 | author = metadataV1.author,
29 | packageVersion = metadataV1.packageVersion,
30 | vendor = metadataV1.vendor,
31 | driverVersion = metadataV1.driverVersion,
32 | minApi = metadataV1.minApi,
33 | description = metadataV1.description,
34 | libraryName = metadataV1.libraryName,
35 | )
36 |
37 | val label get() = if (packageVersion.isEmpty()) name else "$name-v$packageVersion"
38 |
39 | companion object {
40 | private const val SCHEMA_VERSION_V1 = 1
41 |
42 | fun deserialize(metadataFile : File) : GpuDriverMetadata {
43 | val metadataJson = Json.parseToJsonElement(metadataFile.readText())
44 |
45 | return when (metadataJson.jsonObject["schemaVersion"]?.jsonPrimitive?.intOrNull) {
46 | SCHEMA_VERSION_V1 -> GpuDriverMetadata(Json.decodeFromJsonElement(metadataJson))
47 | else -> throw SerializationException("Unsupported metadata version")
48 | }
49 | }
50 | }
51 | }
52 |
53 | @Serializable
54 | private data class GpuDriverMetadataV1(
55 | val schemaVersion : Int,
56 | val name : String,
57 | val author : String,
58 | val packageVersion : String,
59 | val vendor : String,
60 | val driverVersion : String,
61 | val minApi : Int,
62 | val description : String,
63 | val libraryName : String,
64 | )
65 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/utils/DriversFetcher.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3.utils
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import android.util.Log
6 | import io.ktor.client.*
7 | import io.ktor.client.call.*
8 | import io.ktor.client.plugins.contentnegotiation.*
9 | import io.ktor.client.plugins.logging.*
10 | import io.ktor.client.request.*
11 | import io.ktor.client.statement.*
12 | import io.ktor.serialization.kotlinx.json.*
13 | import io.ktor.utils.io.*
14 | import io.ktor.http.HttpHeaders
15 | import kotlinx.coroutines.Dispatchers
16 | import kotlinx.coroutines.withContext
17 | import kotlinx.serialization.Serializable
18 | import kotlinx.serialization.json.Json
19 | import java.io.OutputStream
20 | import java.io.FileOutputStream
21 | import java.io.File
22 |
23 | object DriversFetcher {
24 | private val httpClient = HttpClient {
25 | install(ContentNegotiation) {
26 | json(Json { ignoreUnknownKeys = true })
27 | }
28 | install(Logging) {
29 | level = LogLevel.BODY
30 | }
31 | }
32 |
33 | @Serializable
34 | data class GitHubRelease(
35 | val name: String,
36 | val assets: List = emptyList()
37 | )
38 |
39 | @Serializable
40 | data class Asset(val browser_download_url: String)
41 |
42 | suspend fun fetchReleases(repoUrl: String, bypassValidation: Boolean = false): FetchResultOutput {
43 | val repoPath = repoUrl.removePrefix("https://github.com/")
44 | val validationUrl = "https://api.github.com/repos/$repoPath/contents/.adrenoDrivers"
45 | val apiUrl = "https://api.github.com/repos/$repoPath/releases"
46 |
47 | return try {
48 | val response: HttpResponse = withContext(Dispatchers.IO) {
49 | httpClient.get(apiUrl)
50 | }
51 |
52 | if (response.status.value != 200)
53 | return FetchResultOutput(emptyList(), FetchResult.Error("Failed to fetch drivers"))
54 |
55 | val isValid = withContext(Dispatchers.IO) {
56 | try {
57 | httpClient.get(validationUrl).status.value == 200
58 | } catch (e: Exception) {
59 | false
60 | }
61 | }
62 |
63 | if (!isValid && !bypassValidation) {
64 | return FetchResultOutput(emptyList(), FetchResult.Warning("Provided driver repo url is not valid."))
65 | }
66 |
67 | val releases: List = response.body()
68 | val drivers = releases.map { release ->
69 | val assetUrl = release.assets.firstOrNull()?.browser_download_url
70 | release.name to assetUrl
71 | }
72 | FetchResultOutput(drivers, FetchResult.Success)
73 | } catch (e: Exception) {
74 | Log.e("DriversFetcher", "Error fetching releases: ${e.message}", e)
75 | FetchResultOutput(emptyList(), FetchResult.Error("Error fetching releases: ${e.message}"))
76 | }
77 | }
78 |
79 | suspend fun downloadAsset(
80 | assetUrl: String,
81 | destinationFile: File,
82 | progressCallback: (Long, Long) -> Unit
83 | ): DownloadResult {
84 | return try {
85 | withContext(Dispatchers.IO) {
86 | val response: HttpResponse = httpClient.get(assetUrl)
87 | val contentLength = response.headers[HttpHeaders.ContentLength]?.toLong() ?: -1L
88 |
89 | FileOutputStream(destinationFile)?.use { outputStream ->
90 | writeResponseToStream(response, outputStream, contentLength, progressCallback)
91 | } ?: return@withContext DownloadResult.Error("Failed to open ${destinationFile.absolutePath}")
92 | }
93 | DownloadResult.Success
94 | } catch (e: Exception) {
95 | Log.e("DriversFetcher", "Error downloading file: ${e.message}", e)
96 | DownloadResult.Error(e.message)
97 | }
98 | }
99 |
100 | private suspend fun writeResponseToStream(
101 | response: HttpResponse,
102 | outputStream: OutputStream,
103 | contentLength: Long,
104 | progressCallback: (Long, Long) -> Unit
105 | ) {
106 | val channel = response.bodyAsChannel()
107 | val buffer = ByteArray(1024) // 1KB buffer size
108 | var totalBytesRead = 0L
109 |
110 | while (!channel.isClosedForRead) {
111 | val bytesRead = channel.readAvailable(buffer)
112 | if (bytesRead > 0) {
113 | outputStream.write(buffer, 0, bytesRead)
114 | totalBytesRead += bytesRead
115 | progressCallback(totalBytesRead, contentLength)
116 | }
117 | }
118 | outputStream.flush()
119 | }
120 |
121 | sealed class DownloadResult {
122 | object Success : DownloadResult()
123 | data class Error(val message: String?) : DownloadResult()
124 | }
125 |
126 | data class FetchResultOutput(
127 | val fetchedDrivers: List>,
128 | val result: FetchResult
129 | )
130 |
131 | sealed class FetchResult {
132 | object Success : FetchResult()
133 | data class Error(val message: String?) : FetchResult()
134 | data class Warning(val message: String?) : FetchResult()
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rpcs3/utils/ZipUtil.kt:
--------------------------------------------------------------------------------
1 |
2 | package net.rpcs3.utils
3 |
4 | import java.io.*
5 | import java.util.zip.ZipEntry
6 | import java.util.zip.ZipFile
7 | import java.util.zip.ZipInputStream
8 |
9 | object ZipUtil {
10 |
11 | @Throws(IOException::class)
12 | fun unzip(file : File, targetDirectory : File) {
13 | ZipFile(file).use { zipFile ->
14 | for (zipEntry in zipFile.entries()) {
15 | val destFile = createNewFile(targetDirectory, zipEntry)
16 | // If the zip entry is a file, we need to create its parent directories
17 | val destDirectory : File? = if (zipEntry.isDirectory) destFile else destFile.parentFile
18 |
19 | // Create the destination directory
20 | if (destDirectory == null || (!destDirectory.isDirectory && !destDirectory.mkdirs()))
21 | throw FileNotFoundException("Failed to create destination directory: $destDirectory")
22 |
23 | // If the entry is a directory we don't need to copy anything
24 | if (zipEntry.isDirectory)
25 | continue
26 |
27 | // Copy bytes to destination
28 | try {
29 | zipFile.getInputStream(zipEntry).use { inputStream ->
30 | destFile.outputStream().use { outputStream ->
31 | inputStream.copyTo(outputStream)
32 | }
33 | }
34 | } catch (e : IOException) {
35 | if (destFile.exists())
36 | destFile.delete()
37 | throw e
38 | }
39 | }
40 | }
41 | }
42 |
43 | @Throws(IOException::class)
44 | fun unzip(stream : InputStream, targetDirectory : File) {
45 | ZipInputStream(BufferedInputStream(stream)).use { zis ->
46 | do {
47 | // Get the next entry, break if we've reached the end
48 | val zipEntry = zis.nextEntry ?: break
49 |
50 | val destFile = createNewFile(targetDirectory, zipEntry)
51 | // If the zip entry is a file, we need to create its parent directories
52 | val destDirectory : File? = if (zipEntry.isDirectory) destFile else destFile.parentFile
53 |
54 | // Create the destination directory
55 | if (destDirectory == null || (!destDirectory.isDirectory && !destDirectory.mkdirs()))
56 | throw FileNotFoundException("Failed to create destination directory: $destDirectory")
57 |
58 | // If the entry is a directory we don't need to copy anything
59 | if (zipEntry.isDirectory)
60 | continue
61 |
62 | // Copy bytes to destination
63 | try {
64 | BufferedOutputStream(destFile.outputStream()).use { zis.copyTo(it) }
65 | } catch (e : IOException) {
66 | if (destFile.exists())
67 | destFile.delete()
68 | throw e
69 | }
70 | } while (true)
71 | }
72 | }
73 |
74 | @Throws(IOException::class)
75 | private fun createNewFile(destinationDir : File, zipEntry : ZipEntry) : File {
76 | val destFile = File(destinationDir, zipEntry.name)
77 | val destDirPath = destinationDir.canonicalPath
78 | val destFilePath = destFile.canonicalPath
79 |
80 | if (!destFilePath.startsWith(destDirPath + File.separator))
81 | throw IOException("Entry is outside of the target dir: " + zipEntry.name)
82 |
83 | return destFile
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/circle.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/cross.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/cross.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/dpad_bottom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/dpad_bottom.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/dpad_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/dpad_left.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/dpad_right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/dpad_right.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/dpad_top.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/dpad_top.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_description.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_folder.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_grid_off.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_grid_on.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_osc_off.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_palette.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_play.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_restore.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_rpcs3_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
16 |
18 |
19 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_rpcs3_monochrome.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_show_osc.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_stop.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/l1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/l1.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/l2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/l2.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/l3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/l3.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/left_stick.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/left_stick.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/left_stick_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/left_stick_background.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/r1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/r1.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/r2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/r2.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/r3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/r3.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/right_stick.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/right_stick.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/right_stick_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/right_stick_background.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/rounded_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/select.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/select.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/square.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/start.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/start.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/triangle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/drawable/triangle.png
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
21 |
22 |
27 |
28 |
33 |
34 |
39 |
40 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_rpcs3.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
15 |
16 |
20 |
21 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_rpcs3.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_rpcs3_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_rpcs3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-hdpi/ic_rpcs3.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_rpcs3_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-hdpi/ic_rpcs3_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_rpcs3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-mdpi/ic_rpcs3.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_rpcs3_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-mdpi/ic_rpcs3_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_rpcs3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-xhdpi/ic_rpcs3.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_rpcs3_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-xhdpi/ic_rpcs3_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_rpcs3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-xxhdpi/ic_rpcs3.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_rpcs3_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-xxhdpi/ic_rpcs3_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_rpcs3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-xxxhdpi/ic_rpcs3.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_rpcs3_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/app/src/main/res/mipmap-xxxhdpi/ic_rpcs3_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_rpcs3_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | RPCS3
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/game_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/app/src/test/java/net/rpcs3/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package net.rpcs3
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.kotlin.android) apply false
5 | alias(libs.plugins.compose.compiler) apply false
6 | id("org.jetbrains.kotlin.plugin.serialization") version "1.7.10" apply false
7 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | activityCompose = "1.10.1"
3 | agp = "8.9.0"
4 | coilCompose = "3.1.0"
5 | kotlin = "2.1.10"
6 | coreKtx = "1.15.0"
7 | junit = "4.13.2"
8 | junitVersion = "1.2.1"
9 | espressoCore = "3.6.1"
10 | appcompat = "1.7.0"
11 | kotlinxSerializationJson = "1.8.0"
12 | material = "1.12.0"
13 | constraintlayout = "2.2.1"
14 | navigationCompose = "2.8.8"
15 | activity = "1.10.1"
16 | uiToolingPreviewAndroid = "1.7.8"
17 | uiTooling = "1.7.8"
18 |
19 | [libraries]
20 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
21 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
22 | androidx-material3 = { module = "androidx.compose.material3:material3" }
23 | coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" }
24 | junit = { group = "junit", name = "junit", version.ref = "junit" }
25 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
26 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
27 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
28 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
29 | material = { group = "com.google.android.material", name = "material", version.ref = "material" }
30 | androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
31 | androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
32 | androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
33 | androidx-ui-tooling-preview-android = { group = "androidx.compose.ui", name = "ui-tooling-preview-android", version.ref = "uiToolingPreviewAndroid" }
34 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" }
35 |
36 | [plugins]
37 | android-application = { id = "com.android.application", version.ref = "agp" }
38 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
39 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
40 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RPCS3-Android/rpcs3-android/e2f87a7f14890ac6ba332343b3b811ca1f14e2d1/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Feb 23 17:37:34 MSK 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "RPCS3"
23 | include(":app")
24 |
--------------------------------------------------------------------------------