Skip to content

Commit 913e67c

Browse files
Sirpixelalotclaude
andcommitted
Add background operations with persistent notifications, update to version 1.7.2
New Features: - Background Operations: Operations continue when app is minimized or screen is off - Persistent Notifications: Real-time progress updates in status bar showing file count, current file, and percentage - Foreground Service: OperationService manages all long-running operations (extract, compress, decompile, create) - Completion Notifications: Dismissible notifications remain visible after operations complete for easy review - Notification Permissions: Added POST_NOTIFICATIONS permission support for Android 13+ - Thread Safety: All I/O operations moved to Dispatchers.IO for smooth UI performance Technical Implementation: - New OperationService foreground service with dataSync type - Progress polling every 500ms with notification updates - Initial progress written before service start for instant notification display - STOP_FOREGROUND_DETACH keeps completion notifications visible - Android 13+ notification permission handling in MainActivity - All operations (extraction, compression, decompilation, creation) integrated with service Documentation: - Updated README with comprehensive feature list and usage instructions - Added background operations technical details - Updated requirements and architecture sections - Added FFmpeg-Kit and Sora Editor credits Version: - Updated versionCode to 11 - Updated versionName to "1.7.2" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a2f9e3d commit 913e67c

File tree

6 files changed

+412
-29
lines changed

6 files changed

+412
-29
lines changed

README.md

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,39 @@
11
# Rentool
22

3-
An Android application to extract and create Ren'Py RPA archives and decompile RPYC scripts directly on your device.
3+
A comprehensive Android toolkit for Ren'Py game modding: extract/create RPA archives, decompile RPYC scripts, compress game assets, and edit .rpy files directly on your device.
44

55
## Features
66

7+
### Core Operations
78
- **Extract RPA Archives**: Unpack Ren'Py game archives to access images, scripts, audio, and other assets
8-
- **Create RPA Archives**: Package files and folders into RPA v3 format archives
9+
- **Create RPA Archives**: Package files and folders into RPA v3 format archives with optional auto-creation after compression
910
- **Decompile RPYC Scripts**: Convert compiled .rpyc files back to readable .rpy source scripts
10-
- **Batch Operations**: Extract multiple RPA files or decompile multiple RPYC files at once
11+
- **Compress Games**: Reduce game size with multi-format compression (WebP for images, Opus for audio, H.265 for video)
12+
- **Edit .rpy Scripts**: Built-in syntax-highlighted editor with Ren'Py language support
13+
14+
### Compression Features
15+
- **Image Compression**: Convert PNG/JPG/BMP to WebP with lossless or quality-based compression (1-100%)
16+
- **Audio Compression**: Convert OGG/MP3/WAV/FLAC to Opus with configurable bitrate
17+
- **Video Compression**: Encode MP4/AVI/MKV/WebM to H.265 with quality presets
18+
- **Speed Control**: Choose Fast/Average/Slow compression modes for different performance needs
19+
- **Selective Compression**: Enable/disable specific media types, prevent empty operations
20+
21+
### User Experience
22+
- **Background Operations**: Continue operations when app is minimized with persistent notifications
23+
- **Batch Processing**: Extract multiple RPA files or decompile multiple RPYC files at once
1124
- **Multi-Select Support**: Long-press to select multiple files or folders for batch operations
25+
- **Case-Insensitive Filtering**: Handles uppercase extensions (.RPA, .RPYC) from Windows distributions
1226
- **Smart Directory Defaults**: Output directory automatically defaults to the location of selected input files
13-
- **Real-time Progress Tracking**: View extraction/creation/decompilation progress with file counts, speed, and ETA
27+
- **Accurate Progress Tracking**: Real-time file counts, speed, ETA, and detailed failure reporting
28+
- **Auto-Update Checker**: Notifies when new versions are available on GitHub
1429

1530
## Requirements
1631

17-
- **Android 11 or higher**
32+
- **Android 13 or higher** (API 33+)
1833
- **MANAGE_EXTERNAL_STORAGE permission**: Required for direct file system access
19-
- **Storage**: Enough space for extracted/created archives
34+
- **POST_NOTIFICATIONS permission**: Required for background operation notifications (Android 13+)
35+
- **Storage**: Enough space for extracted/created/compressed archives
36+
- **Processing**: Compression operations benefit from multi-core devices
2037

2138
## Installation
2239

@@ -29,10 +46,11 @@ An Android application to extract and create Ren'Py RPA archives and decompile R
2946

3047
### Prerequisites
3148

32-
- Android Studio Arctic Fox or later
49+
- Android Studio Hedgehog or later
3350
- Android SDK with API 34+
3451
- Python 3.8+ (for Chaquopy build)
3552
- Gradle 8.0+
53+
- FFmpeg-Kit AAR (included in `app/libs/`)
3654

3755
### Build Steps
3856

@@ -103,6 +121,30 @@ python {
103121

104122
**Tip**: Selecting a game's `/game` folder is the fastest way to decompile all scripts at once.
105123

124+
### Compressing Games
125+
126+
1. Tap the **"Compress Game"** card
127+
2. Select the source folder containing game assets (e.g., the `game` folder)
128+
3. Choose an output directory for compressed files
129+
4. Configure compression settings:
130+
- **Image Quality**: 1-100 for lossy, or enable lossless compression
131+
- **Speed Mode**: Fast/Average/Slow (affects lossless compression effort)
132+
- **Audio Quality**: Low/Medium/High bitrate
133+
- **Video Quality**: Low/Medium/High/Very High
134+
- **Threads**: Number of parallel compression tasks
135+
- **Selective Types**: Skip images, audio, or video if desired
136+
- **Auto RPA**: Optionally create RPA archive from compressed output
137+
5. Monitor real-time progress with file counts and compression statistics
138+
6. Failed files are counted as original size for accurate reduction metrics
139+
140+
### Editing .rpy Scripts
141+
142+
1. Tap the **"Edit .rpy"** card
143+
2. Browse and select a `.rpy` file (case-insensitive)
144+
3. Edit code with syntax highlighting and line numbers
145+
4. Save changes directly to the file
146+
5. Return to file picker to open another script
147+
106148
## Technical Details
107149

108150
### RPA Format Support
@@ -118,25 +160,47 @@ python {
118160
- Files are overwritten without prompting during extraction
119161
- Batch creation uses temporary directory for combining multiple sources
120162

163+
### Compression System
164+
165+
- **Image Encoder**: Native Android Bitmap API with WebP format (hardware-accelerated)
166+
- **Audio Encoder**: FFmpeg-Kit with Opus codec
167+
- **Video Encoder**: FFmpeg-Kit with H.265/HEVC codec
168+
- **Parallel Processing**: Configurable thread pool for concurrent image compression
169+
- **Sequential Processing**: Audio and video processed sequentially to manage memory
170+
- **Accurate Metrics**: Tracks actual file counts (X/Y), failed files, and true reduction percentages
171+
172+
### Background Operations
173+
174+
- **Foreground Service**: Operations continue when app is minimized or screen is off
175+
- **Persistent Notifications**: Real-time progress updates in status bar (file count, current file, percentage)
176+
- **Completion Notifications**: Dismissible notifications remain visible after operations complete
177+
- **Thread Safety**: All I/O operations run on Dispatchers.IO to prevent UI freezing
178+
121179
### Progress Tracking
122180

123181
- JSON-based progress file updated in real-time
124-
- Polling interval: 500ms
125-
- Tracks: file count, current file, speed (files/sec), ETA
182+
- Polling interval: 500ms (background operations) / 500ms (UI updates)
183+
- Tracks: file count, current file, speed (files/sec), ETA, batch info, compression statistics
184+
- Case-insensitive file filtering for Windows compatibility (.RPA, .RPYC, .RPY)
126185

127186
## Architecture
128187

129-
- **Language**: Java, Python
130-
- **UI Framework**: Material Design 3
131-
- **Python Integration**: Chaquopy
132-
- **RPA Library**: rpatool.py
133-
- **Decompiler**: unrpyc
134-
- **File Picker**: Custom RecyclerView-based picker with multi-select
188+
- **Language**: Kotlin, Python
189+
- **UI Framework**: Jetpack Compose with Material Design 3
190+
- **Python Integration**: Chaquopy (Python 3.8)
191+
- **RPA Library**: rpatool.py (modified for progress tracking)
192+
- **Decompiler**: unrpyc (CensoredUsername's fork)
193+
- **Compression**: FFmpeg-Kit 6.0 (full build), Android Bitmap API
194+
- **Code Editor**: Sora Editor with TextMate grammar support
195+
- **File Picker**: Jetpack Compose-based picker with multi-select
196+
- **Concurrency**: Kotlin Coroutines with structured concurrency
135197

136198
## Credits
137199

138200
- **RPA Format**: Based on [Ren'Py](https://www.renpy.org/) archive specification
139201
- **Python Integration**: [Chaquopy](https://chaquo.com/chaquopy/)
202+
- **Media Compression**: [FFmpeg-Kit](https://github.com/arthenica/ffmpeg-kit)
203+
- **Code Editor**: [Sora Editor](https://github.com/Rosemoe/sora-editor)
140204
- **Folder Icons**: [Icons8](https://icons8.com/)
141205
- **Rpatool**: [Shizmob](https://codeberg.org/shiz/rpatool)
142206
- **Unrpyc**: [CensoredUsername](https://github.com/CensoredUsername/unrpyc)

app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ android {
2020
applicationId "com.renpytool"
2121
minSdk 33
2222
targetSdk 34
23-
versionCode 10
24-
versionName "1.7.1"
23+
versionCode 11
24+
versionName "1.7.2"
2525

2626
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
2727

app/src/main/AndroidManifest.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
<!-- Network permission for update checker -->
66
<uses-permission android:name="android.permission.INTERNET" />
77

8+
<!-- Foreground service for background operations -->
9+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
10+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
11+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
12+
813
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
914
android:maxSdkVersion="32" />
1015
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
@@ -51,6 +56,12 @@
5156
android:exported="false"
5257
android:theme="@style/Theme.Rentool" />
5358

59+
<service
60+
android:name=".OperationService"
61+
android:enabled="true"
62+
android:exported="false"
63+
android:foregroundServiceType="dataSync" />
64+
5465
</application>
5566

5667
</manifest>

app/src/main/java/com/renpytool/MainActivity.kt

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class MainActivity : ComponentActivity() {
3939

4040
companion object {
4141
private const val PERMISSION_REQUEST_CODE = 100
42+
private const val PERMISSION_REQUEST_NOTIFICATION = 101
4243
}
4344

4445
// ViewModel
@@ -130,6 +131,19 @@ class MainActivity : ComponentActivity() {
130131

131132

132133
private fun checkPermissions() {
134+
// Check notification permission for Android 13+ (for background operation notifications)
135+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
136+
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
137+
!= PackageManager.PERMISSION_GRANTED
138+
) {
139+
ActivityCompat.requestPermissions(
140+
this,
141+
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
142+
PERMISSION_REQUEST_NOTIFICATION
143+
)
144+
}
145+
}
146+
133147
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
134148
// Android 11 and above - use MANAGE_EXTERNAL_STORAGE
135149
if (!Environment.isExternalStorageManager()) {
@@ -176,14 +190,26 @@ class MainActivity : ComponentActivity() {
176190
grantResults: IntArray
177191
) {
178192
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
179-
if (requestCode == PERMISSION_REQUEST_CODE) {
180-
val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
181-
if (!allGranted) {
182-
Toast.makeText(
183-
this,
184-
"Permissions are required for this app to work",
185-
Toast.LENGTH_LONG
186-
).show()
193+
when (requestCode) {
194+
PERMISSION_REQUEST_CODE -> {
195+
val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
196+
if (!allGranted) {
197+
Toast.makeText(
198+
this,
199+
"Permissions are required for this app to work",
200+
Toast.LENGTH_LONG
201+
).show()
202+
}
203+
}
204+
PERMISSION_REQUEST_NOTIFICATION -> {
205+
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
206+
if (!granted) {
207+
Toast.makeText(
208+
this,
209+
"Notification permission is recommended for background operations",
210+
Toast.LENGTH_LONG
211+
).show()
212+
}
187213
}
188214
}
189215
}

app/src/main/java/com/renpytool/MainViewModel.kt

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package com.renpytool
22

33
import android.app.Application
44
import android.content.Context
5+
import android.content.Intent
56
import android.content.SharedPreferences
67
import android.util.Log
8+
import androidx.core.content.ContextCompat
79
import androidx.lifecycle.AndroidViewModel
810
import androidx.lifecycle.viewModelScope
911
import com.chaquo.python.PyObject
@@ -91,6 +93,23 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
9193
}
9294
}
9395

96+
/**
97+
* Start foreground service for background operation support
98+
*/
99+
private fun startOperationService(action: String, sourcePath: String? = null, outputPath: String? = null) {
100+
try {
101+
val intent = Intent(context, OperationService::class.java).apply {
102+
this.action = action
103+
sourcePath?.let { putExtra(OperationService.EXTRA_SOURCE_PATH, it) }
104+
outputPath?.let { putExtra(OperationService.EXTRA_OUTPUT_PATH, it) }
105+
}
106+
ContextCompat.startForegroundService(context, intent)
107+
Log.i("MainViewModel", "Started foreground service for $action")
108+
} catch (e: Exception) {
109+
Log.e("MainViewModel", "Failed to start foreground service", e)
110+
}
111+
}
112+
94113
/**
95114
* Perform batch extraction of RPA files
96115
*/
@@ -100,6 +119,24 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
100119
val tracker = ProgressTracker(context)
101120
tracker.clearProgress()
102121

122+
// Write initial progress BEFORE starting service
123+
val initialData = ProgressData().apply {
124+
operation = "extract"
125+
status = "in_progress"
126+
startTime = System.currentTimeMillis()
127+
lastUpdateTime = System.currentTimeMillis()
128+
totalFiles = 0
129+
processedFiles = 0
130+
currentFile = "Starting batch extraction..."
131+
currentBatchIndex = 1
132+
}
133+
tracker.writeProgress(initialData)
134+
135+
// NOW start foreground service
136+
withContext(Dispatchers.Main) {
137+
startOperationService(OperationService.ACTION_START_EXTRACTION, extractDirPath, extractDirPath)
138+
}
139+
103140
val totalFiles = rpaFilePaths.size
104141
var currentIndex = 1
105142
val batchStartTime = System.currentTimeMillis()
@@ -187,7 +224,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
187224
tracker.clearProgress()
188225

189226
try {
190-
// Initialize progress
227+
// Initialize progress BEFORE starting service
191228
val initialData = ProgressData().apply {
192229
operation = "extract"
193230
status = "in_progress"
@@ -199,6 +236,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
199236
}
200237
tracker.writeProgress(initialData)
201238

239+
// NOW start foreground service (it will immediately see the progress)
240+
withContext(Dispatchers.Main) {
241+
startOperationService(OperationService.ACTION_START_EXTRACTION, rpaFilePath, extractDirPath)
242+
}
243+
202244
// Call Python extraction
203245
val result = rpaModule.callAttr(
204246
"extract_rpa",
@@ -366,7 +408,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
366408
tracker.clearProgress()
367409

368410
try {
369-
// Initialize progress
411+
// Initialize progress BEFORE starting service
370412
val initialData = ProgressData().apply {
371413
operation = "create"
372414
status = "in_progress"
@@ -378,6 +420,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
378420
}
379421
tracker.writeProgress(initialData)
380422

423+
// NOW start foreground service
424+
withContext(Dispatchers.Main) {
425+
startOperationService(OperationService.ACTION_START_CREATION, sourceDirPath, outputFilePath)
426+
}
427+
381428
// Call Python creation
382429
val result = rpaModule.callAttr(
383430
"create_rpa",
@@ -433,7 +480,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
433480
tracker.clearProgress()
434481

435482
try {
436-
// Initialize progress
483+
// Initialize progress BEFORE starting service
437484
val initialData = ProgressData().apply {
438485
operation = "decompile"
439486
status = "in_progress"
@@ -445,6 +492,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
445492
}
446493
tracker.writeProgress(initialData)
447494

495+
// NOW start foreground service
496+
withContext(Dispatchers.Main) {
497+
startOperationService(OperationService.ACTION_START_DECOMPILATION, sourceDirPath)
498+
}
499+
448500
// Call Python decompilation
449501
val result = decompileModule.callAttr(
450502
"decompile_directory",
@@ -559,7 +611,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
559611
tracker.clearProgress()
560612

561613
try {
562-
// Initialize progress
614+
// Initialize progress BEFORE starting service
563615
val initialData = ProgressData().apply {
564616
operation = "compress"
565617
status = "in_progress"
@@ -571,6 +623,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
571623
}
572624
tracker.writeProgress(initialData)
573625

626+
// NOW start foreground service (it will immediately see the progress)
627+
withContext(Dispatchers.Main) {
628+
startOperationService(OperationService.ACTION_START_COMPRESSION, sourceDirPath, outputDirPath)
629+
}
630+
574631
// Create compression manager and perform compression
575632
val compressionManager = CompressionManager(context)
576633
val result = compressionManager.compressGame(

0 commit comments

Comments
 (0)