Skip to content
Merged
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
1 change: 0 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:screenOrientation="portrait"
android:resizeableActivity="true"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
Expand Down
149 changes: 147 additions & 2 deletions android/app/src/main/kotlin/com/mossapps/flick/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.hardware.usb.UsbConstants
import android.hardware.usb.UsbDevice
Expand Down Expand Up @@ -87,6 +86,7 @@ import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.io.OutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.MessageDigest
Expand Down Expand Up @@ -185,7 +185,6 @@ class MainActivity: FlutterActivity() {
}

override fun onCreate(savedInstanceState: Bundle?) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
super.onCreate(savedInstanceState)

if (!nativeInitRustAndroidContext(applicationContext)) {
Expand Down Expand Up @@ -605,6 +604,34 @@ class MainActivity: FlutterActivity() {
result.error("INVALID_ARGUMENT", "folderTreeUri, filePath, and tempFilePath are required", null)
}
}
"stageContentUriForEdit" -> {
val uri = call.argument<String>("uri")
val extensionHint = call.argument<String>("extensionHint")
if (uri != null) {
mainScope.launch {
val staged = withContext(Dispatchers.IO) {
stageContentUriForEdit(uri, extensionHint)
}
result.success(staged)
}
} else {
result.error("INVALID_ARGUMENT", "uri is required", null)
}
}
"writeBytesToContentUri" -> {
val uri = call.argument<String>("uri")
val tempFilePath = call.argument<String>("tempFilePath")
if (uri != null && tempFilePath != null) {
mainScope.launch {
val written = withContext(Dispatchers.IO) {
writeBytesToContentUri(uri, tempFilePath)
}
result.success(written)
}
} else {
result.error("INVALID_ARGUMENT", "uri and tempFilePath are required", null)
}
}
"queryMediaStoreAudio" -> {
val folderPaths = call.argument<List<String>>("folderPaths")
val volumeName = call.argument<String?>("volumeName")
Expand Down Expand Up @@ -1880,6 +1907,124 @@ class MainActivity: FlutterActivity() {
}
}

/**
* Copies a content:// document into a private, uniquely named cache file so
* the Rust tagger can open it by path. The shared playback staging cache is
* deliberately not reused: editing mutates this copy, and a cached playback
* file must never be modified in place.
*/
private fun stageContentUriForEdit(uriString: String, extensionHint: String?): Map<String, Any?> {
val uri = Uri.parse(uriString)
val stagingDir = File(cacheDir, "metadata_edits")
val stagedFile = try {
stagingDir.mkdirs()
pruneMetadataEditStaging(stagingDir)
val extension = resolveStagedExtension(uri, extensionHint)
File(stagingDir, "${md5(uriString)}-${UUID.randomUUID()}.$extension")
} catch (e: Exception) {
Log.w("MainActivity", "stageContentUriForEdit: could not prepare staging: ${e.message}", e)
return mapOf("path" to null, "reason" to "failed")
}

return try {
val input = contentResolver.openInputStream(uri)
if (input == null) {
mapOf("path" to null, "reason" to "not_found")
} else {
input.use { source ->
stagedFile.outputStream().use { target ->
copyStream(source, target)
}
}
if (stagedFile.length() <= 0L) {
stagedFile.delete()
mapOf("path" to null, "reason" to "failed")
} else {
mapOf("path" to stagedFile.absolutePath, "reason" to null)
}
}
} catch (e: java.io.FileNotFoundException) {
stagedFile.delete()
Log.w("MainActivity", "stageContentUriForEdit: not found: $uriString (${e.message})")
mapOf("path" to null, "reason" to "not_found")
} catch (e: SecurityException) {
stagedFile.delete()
Log.w("MainActivity", "stageContentUriForEdit: no access: $uriString (${e.message})")
mapOf("path" to null, "reason" to "no_access")
} catch (e: Exception) {
stagedFile.delete()
Log.w("MainActivity", "stageContentUriForEdit failed: ${e.message}", e)
mapOf("path" to null, "reason" to "failed")
}
}

/** Streams an edited cache copy back over the original document URI. */
private fun writeBytesToContentUri(uriString: String, tempFilePath: String): Map<String, Any?> {
val tempFile = File(tempFilePath)
if (!tempFile.exists()) {
return mapOf("success" to false, "reason" to "failed")
}
val uri = Uri.parse(uriString)

return try {
val output = openContentUriOutputStream(uri)
if (output == null) {
mapOf("success" to false, "reason" to "failed")
} else {
output.use { target ->
tempFile.inputStream().use { source ->
copyStream(source, target)
}
}
mapOf("success" to true, "reason" to "ok")
}
} catch (e: java.io.FileNotFoundException) {
Log.w("MainActivity", "writeBytesToContentUri: not found: $uriString (${e.message})")
mapOf("success" to false, "reason" to "not_found")
} catch (e: SecurityException) {
Log.w("MainActivity", "writeBytesToContentUri: no access: $uriString (${e.message})")
mapOf("success" to false, "reason" to "no_access")
} catch (e: Exception) {
Log.w("MainActivity", "writeBytesToContentUri failed: ${e.message}", e)
mapOf("success" to false, "reason" to "failed")
}
}

/**
* "wt" truncates before writing, but a few document providers only accept
* "w". A genuinely missing document fails again on the retry and is
* reported as not_found by the caller.
*/
private fun openContentUriOutputStream(uri: Uri): OutputStream? {
return try {
contentResolver.openOutputStream(uri, "wt")
} catch (e: IllegalArgumentException) {
contentResolver.openOutputStream(uri, "w")
} catch (e: java.io.FileNotFoundException) {
contentResolver.openOutputStream(uri, "w")
}
}

private fun copyStream(source: InputStream, target: OutputStream) {
val buffer = ByteArray(1024 * 1024)
while (true) {
val read = source.read(buffer)
if (read < 0) break
target.write(buffer, 0, read)
}
target.flush()
}

/** Clear edit copies left behind by crashes; 24h is far beyond one save. */
private fun pruneMetadataEditStaging(stagingDir: File) {
val cutoff = System.currentTimeMillis() - 24 * 60 * 60 * 1000L
stagingDir.listFiles()?.forEach { file ->
if (file.isFile && file.lastModified() < cutoff) {
file.delete()
}
}
}

private fun isIgnoringBatteryOptimizations(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true
Expand Down
9 changes: 8 additions & 1 deletion docs/BAYBAYIN_REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ subsequently turning them off survives a restart.
- The standalone Player Layout settings screen is English in Taglish too,
including both layout names and descriptions, layout controls, quick-action
pickers and album-color options. English and Chinese keep their own copy.
- The UI Customization settings screen is English in Taglish for Track
Thumbnails, Progress Bar and Album Colors, including option names and
descriptions. English and Chinese keep their own copy.
- The album, artist, playlist and smart-mix three-dot More sheets use English,
including their **More** header.
- Album completeness uses English in Taglish: header and grid badges, missing
Expand Down Expand Up @@ -289,7 +292,9 @@ Player Layout has two entry points, and both use the English role in Taglish:
`model_labels.dart` gained caller-catalog `descriptionFor` methods for player
screen modes and album-color modes, so plain extension getters elsewhere still
follow the active locale. The action picker uses the same English catalog as
the sheet.
the sheet. The same pattern covers `ProgressBarStyle` and
`SongTileThumbnailMode`; the UI Customization screen passes its English role
catalog so those option descriptions do not fall back to Taglish.

`sort_filter_bottom_sheet.dart` also uses the English shim. Scoped copy roles
in `songs_screen.dart` keep the album menu, album-options sheet and multiselect
Expand Down Expand Up @@ -353,6 +358,8 @@ Before removing the experimental/draft notices, the Filipino reviewer should:
the fully English full-player three-dot sheet, inline/online lyrics and
its playlist/share/metadata/speed/pitch/sleep/layout sub-interfaces,
the English Player Layout settings screen and action picker,
the English UI Customization Track Thumbnails, Progress Bar and Album
Colors sections,
the fully English Lyrics Sync Studio,
the English Milestones surface and playlist cover editor, the English
playlist detail header information line/lossless indicator, all-English
Expand Down
Loading
Loading