├── .gitignore
├── .idea
├── deploymentTargetSelector.xml
└── inspectionProfiles
│ └── Project_Default.xml
├── README.md
├── app
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── abeja
│ │ └── gamecenterreplacer
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── java
│ │ └── com
│ │ │ └── abeja
│ │ │ └── gamecenterreplacer
│ │ │ ├── BackgroundService.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── UI.kt
│ │ │ ├── ViewModel.kt
│ │ │ ├── data.kt
│ │ │ └── ui
│ │ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ └── res
│ │ ├── drawable
│ │ ├── discord_mark.xml
│ │ ├── github_mark.xml
│ │ ├── ic_launcher_background.xml
│ │ └── ic_launcher_foreground.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── com
│ └── abeja
│ └── gamecenterreplacer
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── img
├── app_icon_transparent.png
├── screenshot_01.jpg
└── screenshot_02.jpg
└── settings.gradle.kts
/.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 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Game Space Replacer
2 |
3 |
4 |

5 |
6 |
11 |
12 |
13 | Take control of the red switch on your REDMAGIC Android device!
14 |
15 |
16 | ## Description
17 |
18 | **Game Space Replacer** is an Android app that allows you to customize the functionality of the red switch on your REDMAGIC device.
19 |
20 | With this app, you can replace the default behavior of the red switch to launch your favorite game or app automatically when triggered.
21 |
22 | ## Features
23 |
24 | - **Custom app selector**: Choose which app should launch when the red switch is pressed.
25 | - **Minimal UI**: Clean, simple interface to manage the red switch functionality.
26 | - **Privacy friendly**: Open source and no internet access, we don't store your data!
27 |
28 | ## Requirements
29 |
30 | - Any Global modern REDMAGIC device running REDMAGIC OS.
31 | - Necessary permissions:
32 | - Usage Stats Permission > Check if the Game Space has been launched.
33 | - Display over other apps permission > Launch a different app on top of the Game Space.
34 |
35 | ## Tested Devices
36 |
37 | The app has been tested on the following devices:
38 |
39 | - ✅ **REDMAGIC 7s Pro**
40 | - ✅ **REDMAGIC 8 Pro**
41 | - ✅ **REDMAGIC 10 Pro**
42 |
43 | It should work on any modern REDMAGIC device. If you encounter any issues, please feel free to open an issue on GitHub. **Note that the Chinese firmware isn't officially supported.**
44 |
45 | ## Common Issues
46 | - **REDMAGIC 10 Devices must have set the switch to launch the Game Space.**
47 | - The REDMAGIC Game Space briefly appears before the selected app launches.
48 |
49 | ## Installation
50 | - Go to the [Releases page](https://github.com/TheRealCrazyfuy/GameSpaceReplacer/releases) and download the latest release.
51 | - Once the download is finished open the `.apk` and install it.
52 | - Then just follow the instructions inside the app.
53 | - **On Android 15+ you have to manually give these permissions from settings. [Learn more](https://www.androidpolice.com/android-15-sideloading-restrictions-bad-users/)**
54 |
55 | ## Contributions
56 | All contributions are welcome, open a pull request and we'll look into it, thanks.
57 |
58 | ## Screenshots
59 |
60 |
61 |
62 | ## Community
63 | Join our Discord community to connect directly with the project, share your feedback, and get the latest updates. Whether you need support or want to contribute, we’d love to have you with us!
64 |
65 | [](https://discord.gg/Hc4UPXqc4j)
66 |
67 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.kotlin.android)
4 | }
5 |
6 | android {
7 | namespace = "com.abeja.gamecenterreplacer"
8 | compileSdk = 35
9 |
10 | defaultConfig {
11 | applicationId = "com.abeja.gamecenterreplacer"
12 | minSdk = 30
13 | targetSdk = 35
14 | versionCode = 2
15 | versionName = "1.1"
16 |
17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
18 | vectorDrawables {
19 | useSupportLibrary = true
20 | }
21 | }
22 |
23 | buildTypes {
24 | release {
25 | isMinifyEnabled = false
26 | proguardFiles(
27 | getDefaultProguardFile("proguard-android-optimize.txt"),
28 | "proguard-rules.pro"
29 | )
30 | }
31 | }
32 | compileOptions {
33 | sourceCompatibility = JavaVersion.VERSION_1_8
34 | targetCompatibility = JavaVersion.VERSION_1_8
35 | }
36 | kotlinOptions {
37 | jvmTarget = "1.8"
38 | }
39 | buildFeatures {
40 | compose = true
41 | buildConfig = true
42 | }
43 | composeOptions {
44 | kotlinCompilerExtensionVersion = "1.5.1"
45 | }
46 | packaging {
47 | resources {
48 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
49 | }
50 | }
51 | lint {
52 | baseline = file("lint-baseline.xml")
53 | }
54 | }
55 |
56 | dependencies {
57 |
58 | implementation(libs.androidx.core.ktx)
59 | implementation(libs.androidx.lifecycle.runtime.ktx)
60 | implementation(libs.androidx.activity.ktx)
61 | implementation(libs.androidx.activity.compose)
62 | implementation(platform(libs.androidx.compose.bom))
63 | implementation(libs.androidx.ui)
64 | implementation(libs.androidx.ui.graphics)
65 | implementation(libs.androidx.ui.tooling.preview)
66 | implementation(libs.androidx.material3)
67 | implementation(libs.androidx.runtime.livedata)
68 | testImplementation(libs.junit)
69 | androidTestImplementation(libs.androidx.junit)
70 | androidTestImplementation(libs.androidx.espresso.core)
71 | androidTestImplementation(platform(libs.androidx.compose.bom))
72 | androidTestImplementation(libs.androidx.ui.test.junit4)
73 | debugImplementation(libs.androidx.ui.tooling)
74 | debugImplementation(libs.androidx.ui.test.manifest)
75 | }
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/abeja/gamecenterreplacer/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.abeja.gamecenterreplacer
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("com.abeja.gamecenterreplacer", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
21 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/abeja/gamecenterreplacer/BackgroundService.kt:
--------------------------------------------------------------------------------
1 | package com.abeja.gamecenterreplacer
2 |
3 | import android.app.NotificationChannel
4 | import android.app.NotificationManager
5 | import android.app.Service
6 | import android.app.usage.UsageEvents
7 | import android.app.usage.UsageStatsManager
8 | import android.content.Context
9 | import android.content.Intent
10 | import android.os.IBinder
11 | import android.provider.Settings
12 | import android.util.Log
13 | import androidx.core.app.NotificationCompat
14 | import kotlinx.coroutines.*
15 | import kotlinx.coroutines.flow.Flow
16 | import kotlinx.coroutines.flow.flow
17 |
18 | class BackgroundService : Service() {
19 | private val targetAppPackage = serviceData.appTargetPackage
20 | private val targetAppName = serviceData.appTargetName
21 | private val triggerAppPackage = "cn.nubia.gamelauncher"
22 | private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
23 |
24 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
25 | //requestUsageStatsPermission()
26 |
27 | val channelId = "GameCenterReplacer"
28 | val channelName = "Background Service"
29 | val notificationChannel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT)
30 | val notificationManager = getSystemService(NotificationManager::class.java)
31 | notificationManager.createNotificationChannel(notificationChannel)
32 |
33 | val notification = NotificationCompat.Builder(this, channelId)
34 | .setContentTitle("Red switch set to: $targetAppName")
35 | //.setContentText("Long press this notification to hide.")
36 | .setSmallIcon(R.mipmap.ic_launcher)
37 | .build()
38 |
39 | startForeground(1, notification)
40 |
41 | serviceScope.launch {
42 | monitorApps().collect { event ->
43 | //Log.d("BackgroundService", "Event: ${event.packageName}")
44 | if ( event.packageName == triggerAppPackage) {
45 | //Log.d("BackgroundService", "Trigger app detected")
46 | //startOrResumeTargetApp()
47 | }
48 | }
49 | }
50 |
51 | return START_STICKY
52 | }
53 |
54 | private fun monitorApps(): Flow = flow {
55 | val usageStatsManager = getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
56 | while (true) {
57 | val endTime = System.currentTimeMillis()
58 | val beginTime = endTime - 5000
59 | val events = usageStatsManager.queryEvents(beginTime, endTime)
60 | val event = UsageEvents.Event()
61 | var currentForegroundApp: String? = null
62 |
63 | while (events.hasNextEvent()) {
64 | events.getNextEvent(event)
65 | if (event.eventType == UsageEvents.Event.MOVE_TO_FOREGROUND) {
66 | currentForegroundApp = event.packageName
67 | }
68 | emit(event)
69 | }
70 |
71 | if (currentForegroundApp == triggerAppPackage) {
72 | Log.d("BackgroundService", "Trigger app is currently on screen")
73 | startOrResumeTargetApp()
74 | }
75 |
76 | delay(1000) // TODO: get rid of this delay while not tanking the battery life //
77 | }
78 | }
79 |
80 | private fun startOrResumeTargetApp() {
81 | val needKillGameLauncher = Settings.Global.getInt(contentResolver, "gcs_need_kill_game_launcher", 0)
82 | if (needKillGameLauncher == 0) { // check if the competitive key is on too
83 | Log.d("BackgroundService", "Attempting to start target app: $targetAppPackage")
84 | val launchIntent = targetAppPackage?.let { packageManager.getLaunchIntentForPackage(it) }
85 | if (launchIntent != null) {
86 | launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
87 | Log.d("BackgroundService", "Launching target app: $targetAppPackage")
88 | startActivity(launchIntent)
89 | } else {
90 | Log.e("BackgroundService", "Failed to find launch intent for package: $targetAppPackage")
91 | }
92 | }
93 |
94 | }
95 |
96 | override fun onBind(intent: Intent?): IBinder? = null
97 |
98 | override fun onDestroy() {
99 | serviceScope.cancel()
100 | super.onDestroy()
101 | }
102 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/abeja/gamecenterreplacer/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.abeja.gamecenterreplacer
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.enableEdgeToEdge
7 | import androidx.activity.viewModels
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.material3.ExperimentalMaterial3Api
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.Scaffold
13 | import androidx.compose.material3.Text
14 | import androidx.compose.material3.TopAppBar
15 | import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.res.stringResource
19 | import androidx.compose.ui.tooling.preview.Preview
20 | import com.abeja.gamecenterreplacer.ui.theme.GamecenterreplacerTheme
21 |
22 | class MainActivity : ComponentActivity() {
23 | private val viewModel: ViewModel by viewModels()
24 |
25 | @OptIn(ExperimentalMaterial3Api::class)
26 | override fun onCreate(savedInstanceState: Bundle?) {
27 | super.onCreate(savedInstanceState)
28 | enableEdgeToEdge()
29 | viewModel.onLaunch(this)
30 | setContent {
31 | GamecenterreplacerTheme {
32 | Scaffold(modifier = Modifier.fillMaxSize(),
33 | topBar = {
34 | TopAppBar(
35 | colors = topAppBarColors(
36 | containerColor = MaterialTheme.colorScheme.primaryContainer,
37 | titleContentColor = MaterialTheme.colorScheme.primary,
38 | ),
39 | title = {
40 | Text(stringResource(R.string.app_name))
41 | }
42 | )
43 | }
44 | ) { innerPadding ->
45 | MainUI(modifier = Modifier.padding(innerPadding), viewModel)
46 | }
47 | }
48 | }
49 | }
50 |
51 | override fun onResume() {
52 | super.onResume()
53 | viewModel.onLaunch(this)
54 | }
55 | }
56 |
57 | @Composable
58 | fun Greeting(name: String, modifier: Modifier = Modifier) {
59 | Text(
60 | text = "Hello $name!",
61 | modifier = modifier
62 | )
63 | }
64 |
65 | @Preview(showBackground = true)
66 | @Composable
67 | fun GreetingPreview() {
68 | GamecenterreplacerTheme {
69 | Greeting("Android")
70 | }
71 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/abeja/gamecenterreplacer/UI.kt:
--------------------------------------------------------------------------------
1 | package com.abeja.gamecenterreplacer
2 |
3 | import android.content.Context
4 | import android.content.pm.ResolveInfo
5 | import androidx.activity.compose.rememberLauncherForActivityResult
6 | import androidx.activity.result.ActivityResultLauncher
7 | import androidx.activity.result.contract.ActivityResultContracts
8 | import androidx.compose.foundation.Image
9 | import androidx.compose.foundation.background
10 | import androidx.compose.foundation.clickable
11 | import androidx.compose.foundation.layout.Arrangement
12 | import androidx.compose.foundation.layout.Column
13 | import androidx.compose.foundation.layout.Row
14 | import androidx.compose.foundation.layout.Spacer
15 | import androidx.compose.foundation.layout.fillMaxWidth
16 | import androidx.compose.foundation.layout.padding
17 | import androidx.compose.foundation.layout.size
18 | import androidx.compose.foundation.layout.width
19 | import androidx.compose.foundation.lazy.LazyColumn
20 | import androidx.compose.foundation.lazy.items
21 | import androidx.compose.material.icons.Icons
22 | import androidx.compose.material.icons.automirrored.filled.List
23 | import androidx.compose.material.icons.filled.Warning
24 | import androidx.compose.material3.AlertDialog
25 | import androidx.compose.material3.Icon
26 | import androidx.compose.material3.MaterialTheme
27 | import androidx.compose.material3.Switch
28 | import androidx.compose.material3.Text
29 | import androidx.compose.material3.TextButton
30 | import androidx.compose.runtime.Composable
31 | import androidx.compose.runtime.getValue
32 | import androidx.compose.runtime.livedata.observeAsState
33 | import androidx.compose.runtime.mutableStateOf
34 | import androidx.compose.runtime.remember
35 | import androidx.compose.ui.Alignment
36 | import androidx.compose.ui.Modifier
37 | import androidx.compose.ui.graphics.Color
38 | import androidx.compose.ui.graphics.asImageBitmap
39 | import androidx.compose.ui.graphics.vector.ImageVector
40 | import androidx.compose.ui.platform.LocalContext
41 | import androidx.compose.ui.res.painterResource
42 | import androidx.compose.ui.unit.dp
43 | import androidx.core.graphics.drawable.toBitmap
44 | import android.Manifest
45 | import android.util.Log
46 |
47 | @Composable
48 | fun MainUI(modifier: Modifier = Modifier, viewModel: ViewModel) {
49 | val context = LocalContext.current
50 | val mainSwitchStatus = remember {
51 | mutableStateOf(
52 | viewModel.checkifServiceisRunning(
53 | context,
54 | BackgroundService::class.java
55 | )
56 | )
57 | }
58 | val showAppsDialog = remember { mutableStateOf(false) }
59 |
60 | // TODO: Figure a better way to update the UI
61 | val isUsageStatsPermissionGranted by viewModel.isUsageStatsPermissionGranted.observeAsState(false)
62 | val isOnTopPermissionGranted by viewModel.isOnTopPermissionGranted.observeAsState(false)
63 | val isNotificationPermissionGranted by viewModel.isNotificationPermissionGranted.observeAsState(false)
64 | val appHasBeenChoosed by viewModel.appHasBeenChoosed.observeAsState(false)
65 | val appTargetName by viewModel.appTargetName.observeAsState("None")
66 |
67 | val requestNotificationLauncher: ActivityResultLauncher = rememberLauncherForActivityResult(
68 | contract = ActivityResultContracts.RequestPermission()
69 | ) { isGranted: Boolean ->
70 | if (isGranted) {
71 | // Permission granted
72 | Log.d("MainUI", "Notification permission granted")
73 | } else {
74 | // Permission denied
75 | Log.d("MainUI", "Notification permission denied")
76 | viewModel.openNotificationPermissionSettings(context)
77 | }
78 | }
79 |
80 | LazyColumn(modifier = modifier
81 | .padding(top = 56.dp)
82 | .fillMaxWidth()) {
83 |
84 | item {
85 | /**
86 | * Permissions check
87 | */
88 | if (!isUsageStatsPermissionGranted) {
89 | StandardText("Usage access is required")
90 | StandardButton(
91 | "Allow usage access",
92 | Icons.Default.Warning
93 | ) { viewModel.requestUsageStatsPermission(context) }
94 | }
95 | if (!isOnTopPermissionGranted) {
96 | StandardText("On top permission is required")
97 | StandardButton(
98 | "Allow on top permission",
99 | Icons.Default.Warning
100 | ) { viewModel.requestOnTopPermission(context) }
101 | }
102 | if (!isNotificationPermissionGranted) {
103 | StandardText("Notification access is required")
104 | StandardButton(
105 | "Allow notification access",
106 | Icons.Default.Warning
107 | ) { requestNotificationLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) }
108 | }
109 | }
110 |
111 | item {
112 | /**
113 | * Main UI
114 | */
115 | StandardText("Turn on the switch below to automatically launch your chosen app whenever you activate the competitive key.")
116 | StandardSwitch(
117 | "Replace Game Space",
118 | mainSwitchStatus.value,
119 | enabled = isUsageStatsPermissionGranted && isOnTopPermissionGranted && isNotificationPermissionGranted && appHasBeenChoosed
120 | ) {
121 | mainSwitchStatus.value = it
122 | viewModel.setServiceStatus(context, mainSwitchStatus.value)
123 | }
124 |
125 | StandardText(
126 | "Chosen app: $appTargetName",
127 | )
128 | StandardButton("Choose a different app", Icons.AutoMirrored.Filled.List) {
129 | showAppsDialog.value = true
130 | }
131 | }
132 |
133 | item {
134 | /**
135 | * App list dialog
136 | */
137 | if (showAppsDialog.value) {
138 | AppListDialog(context, viewModel, { showAppsDialog.value = false }) {
139 | viewModel.setApptarget(
140 | it.activityInfo.packageName,
141 | it.loadLabel(context.packageManager).toString(),
142 | context
143 | )
144 | showAppsDialog.value = false
145 | }
146 | }
147 |
148 | StandardText("Version ${BuildConfig.VERSION_NAME}")
149 |
150 | StandardLinkIcon(
151 | onClickGitHub = { viewModel.openGitHubRepository(context, "https://www.github.com/therealcrazyfuy/GameSpaceReplacer") },
152 | onClickDiscord = { viewModel.openGitHubRepository(context, "https://discord.gg/Hc4UPXqc4j") }
153 | )
154 | }
155 | }
156 | }
157 |
158 | @Composable
159 | fun StandardText(text: String) {
160 | Text(
161 | text = text,
162 | style = MaterialTheme.typography.bodyLarge,
163 | modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
164 | )
165 | }
166 |
167 | @Composable
168 | fun StandardSwitch(
169 | text: String,
170 | checked: Boolean,
171 | enabled: Boolean = true,
172 | onCheckedChange: (Boolean) -> Unit
173 | ) {
174 | Row(
175 | modifier = Modifier
176 | .fillMaxWidth()
177 | .padding(horizontal = 16.dp, vertical = 12.dp)
178 | .clickable(enabled) { onCheckedChange(!checked) },
179 | verticalAlignment = Alignment.CenterVertically
180 | ) {
181 | Text(
182 | text = text,
183 | style = MaterialTheme.typography.bodyLarge,
184 | modifier = Modifier.padding(end = 8.dp)
185 | )
186 | Spacer(modifier = Modifier.weight(1f))
187 | Switch(
188 | checked = checked,
189 | enabled = enabled,
190 | onCheckedChange = {
191 | onCheckedChange(it)
192 | }
193 | )
194 | }
195 | }
196 |
197 | @Composable
198 | fun StandardButton(
199 | text: String,
200 | icon: ImageVector,
201 | modifier: Modifier = Modifier,
202 | onClick: () -> Unit
203 | ) {
204 | Row(
205 | modifier = Modifier
206 | .fillMaxWidth()
207 | .clickable(onClick = onClick)
208 | .padding(horizontal = 16.dp, vertical = 12.dp)
209 | .background(Color.Transparent),
210 | verticalAlignment = Alignment.CenterVertically
211 | ) {
212 | Icon(
213 | imageVector = icon,
214 | contentDescription = null,
215 | modifier = Modifier.size(24.dp)
216 | )
217 | Spacer(modifier = Modifier.width(16.dp))
218 | Text(
219 | text = text,
220 | style = MaterialTheme.typography.bodyLarge,
221 | )
222 | }
223 | }
224 |
225 | @Composable
226 | fun StandardLinkIcon(
227 | onClickGitHub: () -> Unit,
228 | onClickDiscord: () -> Unit
229 | ) {
230 | Row(
231 | modifier = Modifier
232 | .fillMaxWidth()
233 | .padding(horizontal = 16.dp, vertical = 12.dp)
234 | .background(Color.Transparent),
235 | verticalAlignment = Alignment.CenterVertically,
236 | horizontalArrangement = Arrangement.Center
237 | ) {
238 | Icon(
239 | painter = painterResource(id= R.drawable.github_mark),
240 | contentDescription = null,
241 | modifier = Modifier.size(42.dp).padding(end = 8.dp).clickable(onClick = onClickGitHub)
242 | )
243 | Icon(
244 | painter = painterResource(id= R.drawable.discord_mark),
245 | contentDescription = null,
246 | modifier = Modifier.size(42.dp).padding(end = 8.dp).clickable(onClick = onClickDiscord)
247 | )
248 |
249 | }
250 | }
251 |
252 | /**
253 | * Dialog to choose a different app
254 | */
255 | @Composable
256 | fun AppListDialog(
257 | context: Context,
258 | viewModel: ViewModel,
259 | onDismiss: () -> Unit,
260 | onAppSelected: (ResolveInfo) -> Unit
261 | ) {
262 | val launchableApps = remember {
263 | viewModel.getLaunchableApps(context)
264 | .sortedBy { it.loadLabel(context.packageManager).toString() }
265 | }
266 | val packageManager = context.packageManager
267 |
268 | AlertDialog(
269 | onDismissRequest = onDismiss,
270 | title = {
271 | Text(text = "Choose an app to launch")
272 | },
273 | text = {
274 | LazyColumn {
275 | items(launchableApps) { appInfo ->
276 | val appName = appInfo.loadLabel(packageManager).toString()
277 | Row(
278 | modifier = Modifier
279 | .fillMaxWidth()
280 | .clickable { onAppSelected(appInfo) }
281 | .padding(8.dp),
282 | verticalAlignment = Alignment.CenterVertically
283 | ) {
284 | Image(
285 | bitmap = appInfo.loadIcon(packageManager).toBitmap().asImageBitmap(),
286 | contentDescription = null,
287 | modifier = Modifier.size(40.dp)
288 | )
289 | Spacer(modifier = Modifier.width(8.dp))
290 | Column {
291 | Text(text = appName)
292 | Text(text = appInfo.activityInfo.packageName)
293 | }
294 | }
295 | }
296 | }
297 |
298 | },
299 | confirmButton = {
300 | TextButton(onClick = onDismiss) {
301 | Text("Close")
302 | }
303 | }
304 | )
305 | }
306 |
--------------------------------------------------------------------------------
/app/src/main/java/com/abeja/gamecenterreplacer/ViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.abeja.gamecenterreplacer
2 |
3 | import android.app.ActivityManager
4 | import android.app.AppOpsManager
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.content.pm.PackageManager
8 | import android.content.pm.ResolveInfo
9 | import android.net.Uri
10 | import android.os.Build
11 | import android.provider.Settings
12 | import android.util.Log
13 | import androidx.core.content.ContextCompat
14 | import androidx.lifecycle.LiveData
15 | import androidx.lifecycle.MutableLiveData
16 | import androidx.lifecycle.ViewModel
17 |
18 | class ViewModel : ViewModel() {
19 | private val _isUsageStatsPermissionGranted = MutableLiveData()
20 | val isUsageStatsPermissionGranted: LiveData get() = _isUsageStatsPermissionGranted
21 |
22 | private val _isOnTopPermissionGranted = MutableLiveData()
23 | val isOnTopPermissionGranted: LiveData get() = _isOnTopPermissionGranted
24 |
25 | private val _isNotificationPermissionGranted = MutableLiveData()
26 | val isNotificationPermissionGranted: LiveData get() = _isNotificationPermissionGranted
27 |
28 | private val _appHasBeenChoosed = MutableLiveData()
29 | val appHasBeenChoosed: LiveData get() = _appHasBeenChoosed
30 |
31 | private val _appTargetName = MutableLiveData()
32 | val appTargetName: LiveData get() = _appTargetName
33 |
34 | fun getServiceData() : serviceData {
35 | return serviceData
36 | }
37 |
38 | fun onLaunch(context: Context) {
39 | val appTargetPackage = getPreferences(context, "appTargetPackage") ?: ""
40 | val appTargetName = getPreferences(context, "appTargetName") ?: ""
41 |
42 | if (appTargetPackage.isNotEmpty() && appTargetName.isNotEmpty()) { // only load preferences if an app has been already saved
43 | setApptarget(appTargetPackage, appTargetName, context)
44 | } else {
45 | Log.d("ViewModel", "No app target set initially.")
46 | }
47 | Log.d("ViewModel", "Launched viewmodel")
48 | _isUsageStatsPermissionGranted.value = isUsageStatsPermissionGranted(context)
49 | _isOnTopPermissionGranted.value = isOnTopPermissionGranted(context)
50 | _isNotificationPermissionGranted.value = isNotificationPermissionGranted(context)
51 | }
52 |
53 | fun setApptarget(packageName: String, appName: String, context: Context) {
54 | if (serviceData.appTargetPackage != packageName && checkifServiceisRunning(context, BackgroundService::class.java)) { // only restart the service if the app is different
55 | stopService(context)
56 | startService(context)
57 | }
58 | serviceData.appTargetPackage = packageName
59 | serviceData.appTargetName = appName
60 | _appHasBeenChoosed.value = getApptargetPackage() != null
61 | _appTargetName.value = getApptargetName() ?: ""
62 | // Save preferences
63 | savePreferences(context, "appTargetPackage", packageName)
64 | savePreferences(context, "appTargetName", appName)
65 | }
66 |
67 | fun getApptargetPackage(): String? {
68 | return serviceData.appTargetPackage
69 | }
70 |
71 | fun getApptargetName(): String? {
72 | return serviceData.appTargetName
73 | }
74 |
75 | private fun startService(context: Context) {
76 | val serviceIntent = Intent(context, BackgroundService::class.java)
77 | context.startForegroundService(serviceIntent)
78 | serviceData.serviceStatus = checkifServiceisRunning(context, BackgroundService::class.java)
79 | }
80 |
81 | private fun stopService(context: Context) {
82 | val serviceIntent = Intent(context, BackgroundService::class.java)
83 | context.stopService(serviceIntent)
84 | serviceData.serviceStatus = checkifServiceisRunning(context, BackgroundService::class.java)
85 | }
86 |
87 | fun checkifServiceisRunning(context: Context, serviceClass: Class<*>): Boolean {
88 | val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
89 | for (service in activityManager.getRunningServices(Int.MAX_VALUE)) {
90 | if (serviceClass.name == service.service.className) {
91 | return true
92 | }
93 | }
94 | return false
95 | }
96 |
97 | fun setServiceStatus(context: Context, serviceStatus: Boolean) {
98 | if (serviceData.serviceStatus != serviceStatus) {
99 | serviceData.serviceStatus = serviceStatus
100 | if (serviceStatus) {
101 | startService(context)
102 | } else {
103 | stopService(context)
104 | }
105 | }
106 | }
107 |
108 | fun isUsageStatsPermissionGranted(context: Context): Boolean {
109 | val appOpsManager = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
110 | val mode =
111 | appOpsManager.unsafeCheckOpNoThrow(
112 | AppOpsManager.OPSTR_GET_USAGE_STATS,
113 | android.os.Process.myUid(),
114 | context.packageName
115 | )
116 | return mode == AppOpsManager.MODE_ALLOWED
117 | }
118 |
119 | fun requestUsageStatsPermission(context: Context) {
120 | val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
121 | context.startActivity(intent)
122 | }
123 |
124 | fun isOnTopPermissionGranted(context: Context): Boolean {
125 | val appOpsManager = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
126 | val mode =
127 | appOpsManager.unsafeCheckOpNoThrow(
128 | AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW,
129 | android.os.Process.myUid(),
130 | context.packageName
131 | )
132 | return mode == AppOpsManager.MODE_ALLOWED
133 | }
134 |
135 | fun requestOnTopPermission(context: Context) {
136 | val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
137 | context.startActivity(intent)
138 | }
139 |
140 | fun isNotificationPermissionGranted(context: Context): Boolean {
141 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
142 | ContextCompat.checkSelfPermission(
143 | context,
144 | android.Manifest.permission.POST_NOTIFICATIONS
145 | ) == PackageManager.PERMISSION_GRANTED
146 | } else {
147 | // For versions below Android 13, notification permission is implicitly granted.
148 | true
149 | }
150 | }
151 |
152 | fun openNotificationPermissionSettings(context: Context) {
153 | val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
154 | putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
155 | }
156 | context.startActivity(intent)
157 | }
158 |
159 | fun getLaunchableApps(context: Context): List {
160 | val packageManager = context.packageManager
161 | val intent = Intent(Intent.ACTION_MAIN, null).apply {
162 | addCategory(Intent.CATEGORY_LAUNCHER)
163 | }
164 | return packageManager.queryIntentActivities(intent, 0)
165 | }
166 |
167 | fun openGitHubRepository(context: Context, repositoryUrl: String) {
168 | val intent = Intent(Intent.ACTION_VIEW, Uri.parse(repositoryUrl))
169 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
170 | context.startActivity(intent)
171 | }
172 |
173 | fun savePreferences(context: Context, key: String, value: String) {
174 | val sharedPref = context.getSharedPreferences("com.abeja.gamecenterreplacer", Context.MODE_PRIVATE)
175 | with (sharedPref.edit()) {
176 | putString(key, value)
177 | apply()
178 | }
179 | }
180 |
181 | fun getPreferences(context: Context, key: String): String? {
182 | val sharedPref = context.getSharedPreferences("com.abeja.gamecenterreplacer", Context.MODE_PRIVATE)
183 | return sharedPref.getString(key, null)
184 | }
185 |
186 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/abeja/gamecenterreplacer/data.kt:
--------------------------------------------------------------------------------
1 | package com.abeja.gamecenterreplacer
2 |
3 |
4 | object serviceData {
5 | var serviceStatus = false
6 | var appTargetPackage: String? = null
7 | var appTargetName: String? = null
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/abeja/gamecenterreplacer/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.abeja.gamecenterreplacer.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/java/com/abeja/gamecenterreplacer/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.abeja.gamecenterreplacer.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.platform.LocalContext
13 |
14 | private val DarkColorScheme = darkColorScheme(
15 | primary = Purple80,
16 | secondary = PurpleGrey80,
17 | tertiary = Pink80
18 | )
19 |
20 | private val LightColorScheme = lightColorScheme(
21 | primary = Purple40,
22 | secondary = PurpleGrey40,
23 | tertiary = Pink40
24 |
25 | /* Other default colors to override
26 | background = Color(0xFFFFFBFE),
27 | surface = Color(0xFFFFFBFE),
28 | onPrimary = Color.White,
29 | onSecondary = Color.White,
30 | onTertiary = Color.White,
31 | onBackground = Color(0xFF1C1B1F),
32 | onSurface = Color(0xFF1C1B1F),
33 | */
34 | )
35 |
36 | @Composable
37 | fun GamecenterreplacerTheme(
38 | darkTheme: Boolean = isSystemInDarkTheme(),
39 | // Dynamic color is available on Android 12+
40 | dynamicColor: Boolean = true,
41 | content: @Composable () -> Unit
42 | ) {
43 | val colorScheme = when {
44 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
45 | val context = LocalContext.current
46 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
47 | }
48 |
49 | darkTheme -> DarkColorScheme
50 | else -> LightColorScheme
51 | }
52 |
53 | MaterialTheme(
54 | colorScheme = colorScheme,
55 | typography = Typography,
56 | content = content
57 | )
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/abeja/gamecenterreplacer/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.abeja.gamecenterreplacer.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/discord_mark.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/github_mark.xml:
--------------------------------------------------------------------------------
1 |
6 |
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/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/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_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #000000
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Game Space Replacer
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/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/test/java/com/abeja/gamecenterreplacer/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.abeja.gamecenterreplacer
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 | }
--------------------------------------------------------------------------------
/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 | activityKtx = "1.10.0"
3 | agp = "8.6.0"
4 | kotlin = "1.9.0"
5 | coreKtx = "1.15.0"
6 | junit = "4.13.2"
7 | junitVersion = "1.2.1"
8 | espressoCore = "3.6.1"
9 | lifecycleRuntimeKtx = "2.8.7"
10 | activityCompose = "1.9.3"
11 | composeBom = "2024.04.01"
12 | runtimeLivedata = "1.7.6"
13 |
14 | [libraries]
15 | androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" }
16 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
17 | junit = { group = "junit", name = "junit", version.ref = "junit" }
18 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
19 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
20 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
21 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
22 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
23 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
24 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
25 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
26 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
27 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
28 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
29 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
30 | androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" }
31 |
32 | [plugins]
33 | android-application = { id = "com.android.application", version.ref = "agp" }
34 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
35 |
36 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Jan 14 21:50:48 CET 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-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 |
--------------------------------------------------------------------------------
/img/app_icon_transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/img/app_icon_transparent.png
--------------------------------------------------------------------------------
/img/screenshot_01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/img/screenshot_01.jpg
--------------------------------------------------------------------------------
/img/screenshot_02.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheRealCrazyfuy/GameSpaceReplacer/983b1c8ea25be482d5fd444589988202e2a62cdd/img/screenshot_02.jpg
--------------------------------------------------------------------------------
/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 = "gamecenterreplacer"
23 | include(":app")
24 |
--------------------------------------------------------------------------------