Skip to content

Implement 4 TODO's from the list #402

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ In no particular order:
- [ ] Own custom keyboard overlay, maybe even per-content configs
- Not an overlay, and not per-content, but custom keyboard is there
- [ ] Error/panic handling
- [ ] Loading "animation" (spinner)
- [X] Loading "animation" (spinner)
- [ ] Alternative audio backend (OpenSL ES) for Android < 8
- [ ] Proper storage backend?
- [ ] Resolve design glitches/styling/theming (immersive mode, window insets for holes/notches/corners)
- [X] Resolve design glitches/styling/theming (immersive mode, window insets for holes/notches/corners)
- [ ] Publish to various app stores, maybe automatically?
- [ ] Bundle demo animations/games
- [X] Bundle demo animations/games
- [ ] Add ability to load content from well known online collections? (well maybe not z0r... unless?)
- [ ] History, favorites, other flair...?
- [X] History, favorites, other flair...?

### DONE:

Expand Down Expand Up @@ -73,7 +73,7 @@ In no particular order:
- [X] Unglitchify audio volume (buttons unresponsive?)
- (pending: https://github.com/rust-windowing/winit/pull/1919)
- actually solved by switching to GameActivity instead
- [ ] Register Ruffle to open .swf files
- [X] Register Ruffle to open .swf files
- How well this works depends on the application opening the file, but it "should work most of the time"
- [X] Figure out why videos are not playing (could be a seeking issue)
- The video decoder features weren't enabled on `ruffle_core`...
Expand Down
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ plugins {
alias(libs.plugins.jetbrainsKotlinAndroid)
alias(libs.plugins.cargoNdkAndroid)
alias(libs.plugins.composeCompiler)
kotlin("plugin.serialization") version "1.9.10"
}

android {
Expand Down Expand Up @@ -105,12 +106,14 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation("androidx.compose.material:material-icons-extended:1.6.0")
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.navigation.runtime.ktx)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.games.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.appcompat)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
androidTestImplementation(libs.androidx.uiautomator)
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.test.rules)
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,14 @@
<data android:pathSuffix="swf" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
Binary file added app/src/main/assets/demos/alien_hominid.swf
Binary file not shown.
Binary file added app/src/main/assets/demos/bitey1.swf
Binary file not shown.
Binary file added app/src/main/assets/demos/flyguy.swf
Binary file not shown.
Binary file added app/src/main/assets/demos/marvin_spectrum.swf
Binary file not shown.
Binary file not shown.
Binary file added app/src/main/assets/demos/synj1.swf
Binary file not shown.
Binary file added app/src/main/assets/demos/synj2.swf
Binary file not shown.
Binary file added app/src/main/assets/demos/wasted_sky.swf
Binary file not shown.
224 changes: 224 additions & 0 deletions app/src/main/java/rs/ruffle/DemosManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package rs.ruffle

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.compose.runtime.mutableStateListOf
import androidx.core.content.FileProvider
import java.io.File
import java.io.FileOutputStream

/**
* Manages bundled demo SWF files
*/
object DemosManager {
private val demoEntries = mutableStateListOf<DemoEntry>()
private const val DEMOS_DIRECTORY = "demos"

data class DemoEntry(
val name: String,
val assetPath: String,
val description: String,
val category: DemoCategory
)

enum class DemoCategory {
ANIMATION,
GAME
}

// Map of filename to proper display name and category
private val demoMetadata = mapOf(
"bitey1.swf" to Pair("Bitey of Brackenwood", DemoCategory.ANIMATION),
"saturday_morning_watchmen.swf" to Pair(
"Saturday Morning Watchmen",
DemoCategory.ANIMATION
),
"synj1.swf" to Pair("Synj vs. Horrid Part 1", DemoCategory.ANIMATION),
"synj2.swf" to Pair("Synj vs. Horrid Part 2", DemoCategory.ANIMATION),
"alien_hominid.swf" to Pair("Alien Hominid", DemoCategory.GAME),
"flyguy.swf" to Pair("FlyGuy", DemoCategory.GAME),
"marvin_spectrum.swf" to Pair("Marvin Spectrum", DemoCategory.GAME),
"wasted_sky.swf" to Pair("Wasted Sky", DemoCategory.GAME)
)

/**
* Load demo files from assets
*/
fun loadDemos(context: Context) {
try {
// Clear previous entries
demoEntries.clear()

// Check if the demos directory exists
try {
// Get list of files in the demos directory
val demoFiles = context.assets.list(DEMOS_DIRECTORY)

if (demoFiles != null && demoFiles.isNotEmpty()) {
Log.d("DemosManager", "Found ${demoFiles.size} files in demos directory")

// Add each .swf file as a demo entry
for (file in demoFiles) {
if (file.endsWith(".swf", ignoreCase = true)) {
val path = "$DEMOS_DIRECTORY/$file"

// Get proper name and category from metadata or generate default
val (displayName, category) = demoMetadata[file] ?: Pair(
file.substringBeforeLast(".").replace("_", " ").capitalize(),
DemoCategory.ANIMATION
// Default category if unknown
)

// Verify file can be opened
try {
context.assets.open(path).close()

// Create appropriate description based on category
val description = when (category) {
DemoCategory.ANIMATION -> "Flash Animation"
DemoCategory.GAME -> "Flash Game"
}

demoEntries.add(
DemoEntry(
displayName,
path,
description,
category
)
)
Log.d(
"DemosManager",
"Added demo: $displayName (${category.name}) from $path"
)
} catch (e: Exception) {
Log.e("DemosManager", "Error verifying demo file $path", e)
}
}
}
} else {
Log.w(
"DemosManager",
"No files found in demos directory or directory doesn't exist"
)
}
} catch (e: Exception) {
Log.e("DemosManager", "Error listing demos directory", e)
}

// If no demos found in assets, log this instead of adding placeholder
if (demoEntries.isEmpty()) {
Log.w("DemosManager", "No demo SWF files found in assets")
}

Log.d("DemosManager", "Loaded ${demoEntries.size} demo entries")
} catch (e: Exception) {
Log.e("DemosManager", "Error loading demos", e)
}
}

fun getDemoEntries(): List<DemoEntry> = demoEntries

fun getAnimationDemos(): List<DemoEntry> =
demoEntries.filter { it.category == DemoCategory.ANIMATION }

fun getGameDemos(): List<DemoEntry> = demoEntries.filter { it.category == DemoCategory.GAME }

/**
* Extracts a demo SWF file to a temporary file and returns a Uri to it
*/
fun extractDemoToFile(context: Context, assetPath: String): Uri? {
try {
// First check if the asset actually exists
try {
// This will throw an exception if the asset doesn't exist
context.assets.open(assetPath).close()
} catch (e: Exception) {
Log.e("DemosManager", "Demo file $assetPath doesn't exist in assets", e)
return null
}

val tempFile = File(context.cacheDir, "demos/${File(assetPath).name}")
if (!tempFile.parentFile?.exists()!!) {
val dirCreated = tempFile.parentFile?.mkdirs()
if (dirCreated != true) {
Log.e("DemosManager", "Failed to create parent directory for $tempFile")
return null
}
}

// If the file already exists, delete it to avoid issues
if (tempFile.exists()) {
tempFile.delete()
}

try {
context.assets.open(assetPath).use { inputStream ->
FileOutputStream(tempFile).use { outputStream ->
val buffer = ByteArray(1024)
var length: Int
var totalBytes = 0

while (inputStream.read(buffer).also { length = it } > 0) {
outputStream.write(buffer, 0, length)
totalBytes += length
}

outputStream.flush()
Log.d(
"DemosManager",
"Successfully extracted $assetPath to $tempFile ($totalBytes bytes)"
)
}
}

// Verify the file was created successfully
if (!tempFile.exists() || tempFile.length() == 0L) {
Log.e("DemosManager", "Extraction completed but file is empty or doesn't exist")
return null
}

// Ensure the file is readable
if (!tempFile.canRead()) {
Log.e("DemosManager", "Extracted file is not readable: $tempFile")
return null
}

// Set read permissions for all
tempFile.setReadable(true, false)

// Use FileProvider to create a content:// URI instead of file:// URI
val contentUri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
tempFile
)

// Grant read permission for the content URI
context.grantUriPermission(
context.packageName,
contentUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)

Log.d("DemosManager", "Created content URI: $contentUri for file: $tempFile")
return contentUri
} catch (e: Exception) {
Log.e("DemosManager", "Error extracting demo file $assetPath", e)
return null
}
} catch (e: Exception) {
Log.e("DemosManager", "Error creating temp file", e)
return null
}
}

// Helper function to capitalize first letter of words
private fun String.capitalize(): String {
return this.split(" ").joinToString(" ") { word ->
if (word.isNotEmpty()) word[0].uppercase() + word.substring(1) else ""
}
}
}
81 changes: 81 additions & 0 deletions app/src/main/java/rs/ruffle/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,104 @@ package rs.ruffle
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import java.io.File
import rs.ruffle.ui.theme.RuffleTheme

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)

// Verify demos directory exists in assets or cache
verifyDemosDirectory()

// Load SWF history and favorites data
SwfHistoryManager.loadHistory(this)
SwfFavoritesManager.loadFavorites(this)

// Load bundled demo SWF files
DemosManager.loadDemos(this)

setContent {
RuffleTheme {
RuffleNavHost(openSwf = { openSwf(it) })
}
}
}

private fun verifyDemosDirectory() {
try {
// Check if demos directory exists
val demoFiles = assets.list("demos")

if (demoFiles == null || demoFiles.isEmpty()) {
Log.w(
"MainActivity",
"No demos directory or it's empty. Creating cache directory for demos."
)

// Create a demos directory in the cache folder so we can still show the demos UI section
val demosDir = File(cacheDir, "demos")
if (!demosDir.exists()) {
demosDir.mkdirs()
}

// Inform the user about missing demos
Toast.makeText(
this,
"No demo Flash files found. The demo section will be empty.",
Toast.LENGTH_LONG
).show()
} else {
Log.d("MainActivity", "Found ${demoFiles.size} files in demos directory")

// Verify that at least one .swf file exists
var hasSWF = false
for (file in demoFiles) {
if (file.endsWith(".swf", ignoreCase = true)) {
try {
assets.open("demos/$file").close()
hasSWF = true
break
} catch (e: Exception) {
Log.e("MainActivity", "Error verifying demo file demos/$file", e)
}
}
}

if (!hasSWF) {
Log.w("MainActivity", "No .swf files found in demos directory")

// Inform the user about no SWF files
Toast.makeText(
this,
"Demo section will not work: no valid Flash files found.",
Toast.LENGTH_LONG
).show()
} else {
Log.d(
"MainActivity",
"Found SWF files in demos directory - demo section will work"
)
}
}
} catch (e: Exception) {
Log.e("MainActivity", "Error verifying demos directory", e)

// Inform user about the error
Toast.makeText(
this,
"Error checking for demo files: ${e.localizedMessage}",
Toast.LENGTH_LONG
).show()
}
}

private fun openSwf(uri: Uri) {
val intent = Intent(
this@MainActivity,
Expand Down
Loading
Loading