Integrate ASR/Omni fixes, API Route, Android IME and confirmation-based vocabulary learning. Complete settings transitions and macOS single-item credential storage. Synchronize all application versions to 2.0.0-Beta.4+build.20260930. Integrated PRs: #1116, #1120, #1121, #1123, #1124, #1067 and #1128. Linux #1117 remains independent and is excluded from automatic release. Validated: full cross-platform CI, 18 UI motion cases, 112 frontend test files, 1245 Core tests (1 ignored) and 572 macOS native tests. Co-authored-by: HKLHaoBin <we3q@qq.com>
106 lines
4.1 KiB
Kotlin
106 lines
4.1 KiB
Kotlin
package com.openless.app
|
|
|
|
import android.content.Context
|
|
import android.content.ContentValues
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.os.Environment
|
|
import android.provider.MediaStore
|
|
import android.util.Log
|
|
import androidx.annotation.Keep
|
|
import java.io.File
|
|
import java.io.IOException
|
|
|
|
/**
|
|
* Writes bytes to a SAF content:// URI via ContentResolver.
|
|
*
|
|
* Prefer this over tauri-plugin-fs for exports: fs detaches the FD early and some providers
|
|
* finalize a 0-byte file before Rust finishes writing.
|
|
*/
|
|
@Keep
|
|
object OpenLessContentWriter {
|
|
private const val TAG = "OpenLessContentWriter"
|
|
|
|
@Keep
|
|
@JvmStatic
|
|
fun writeBytes(context: Context, uriString: String, bytes: ByteArray): Boolean {
|
|
return try {
|
|
val uri = Uri.parse(uriString)
|
|
context.contentResolver.openOutputStream(uri)?.use { output ->
|
|
output.write(bytes)
|
|
output.flush()
|
|
}
|
|
?: run {
|
|
Log.w(TAG, "openOutputStream returned null for selected document")
|
|
return false
|
|
}
|
|
Log.i(TAG, "wrote ${bytes.size} bytes to selected document")
|
|
true
|
|
} catch (error: Throwable) {
|
|
Log.e(TAG, "failed to write selected document", error)
|
|
false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Writes an app-generated export into Downloads without a SAF picker.
|
|
*
|
|
* Android 10+ uses MediaStore public Downloads (no storage permission).
|
|
* Older Android writes to the app-specific external Downloads directory
|
|
* instead — public Downloads would need WRITE_EXTERNAL_STORAGE which this
|
|
* app does not declare.
|
|
*/
|
|
@Keep
|
|
@JvmStatic
|
|
fun writePublicDownload(context: Context, fileName: String, bytes: ByteArray): String {
|
|
val safeName = fileName
|
|
.substringAfterLast('/')
|
|
.substringAfterLast('\\')
|
|
.trim()
|
|
.takeIf { it.isNotEmpty() && it != "." && it != ".." }
|
|
?: "openless-error.log"
|
|
@Suppress("DEPRECATION")
|
|
val displayFile = File(
|
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
|
safeName,
|
|
)
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
val values = ContentValues().apply {
|
|
put(MediaStore.MediaColumns.DISPLAY_NAME, safeName)
|
|
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
|
|
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
|
}
|
|
val resolver = context.contentResolver
|
|
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
|
|
?: throw IOException("MediaStore failed to create Downloads item")
|
|
try {
|
|
resolver.openOutputStream(uri)?.use { output ->
|
|
output.write(bytes)
|
|
output.flush()
|
|
} ?: throw IOException("MediaStore returned no output stream")
|
|
} catch (error: Throwable) {
|
|
resolver.delete(uri, null, null)
|
|
throw error
|
|
}
|
|
Log.i(TAG, "wrote ${bytes.size} bytes to public Downloads: $displayFile")
|
|
return displayFile.absolutePath
|
|
}
|
|
|
|
// Pre-Android 10: public Downloads needs WRITE_EXTERNAL_STORAGE which
|
|
// this app does not declare. Write to app-specific external Downloads
|
|
// instead (no runtime permission; still a real on-device .log path).
|
|
val appDownloads = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
|
?: throw IOException("app-specific Downloads directory unavailable")
|
|
if (!appDownloads.exists() && !appDownloads.mkdirs()) {
|
|
throw IOException("failed to create app-specific Downloads directory")
|
|
}
|
|
val appFile = File(appDownloads, safeName)
|
|
appFile.outputStream().use { output ->
|
|
output.write(bytes)
|
|
output.flush()
|
|
}
|
|
Log.i(TAG, "wrote ${bytes.size} bytes to app Downloads (pre-Q): $appFile")
|
|
return appFile.absolutePath
|
|
}
|
|
} |