Skip to content

Commit

Permalink
Use an isolatedProcess service for Rust functions FileViewModel calls
Browse files Browse the repository at this point in the history
  • Loading branch information
soupslurpr committed Dec 22, 2023
1 parent f3b5a96 commit de6f3dc
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 23 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ android {
}
buildFeatures {
compose = true
aidl = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.7"
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
android:enableOnBackInvokedCallback="true"
android:memtagMode="async"
tools:targetApi="34">
<service
android:name=".ui.FileViewModelRustLibraryIsolatedService"
android:exported="false"
android:isolatedProcess="true"
android:process=":file_view_model_rust_library_isolated_process"></service>
<activity
android:name="dev.soupslurpr.beautyxt.MainActivity"
android:exported="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// IFileViewModelRustLibraryAidlInterface.aidl
package dev.soupslurpr.beautyxt;

// Declare any non-default types here with import statements

interface IFileViewModelRustLibraryAidlInterface {
String markdownToHtml(String markdown);
byte[] markdownToDocx(String markdown);
byte[] plainTextToDocx(String plainText);
}
7 changes: 6 additions & 1 deletion app/src/main/kotlin/dev/soupslurpr/beautyxt/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import dev.soupslurpr.beautyxt.settings.PreferencesViewModel
import dev.soupslurpr.beautyxt.ui.FileViewModel
import dev.soupslurpr.beautyxt.ui.ReviewPrivacyPolicyAndLicense
import dev.soupslurpr.beautyxt.ui.TypstProjectViewModel
import dev.soupslurpr.beautyxt.ui.theme.BeauTyXTTheme

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
Expand All @@ -27,10 +28,13 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)

setContent {
val fileViewModel: FileViewModel = viewModel()

val typstProjectViewModel: TypstProjectViewModel = viewModel()

val preferencesViewModel: PreferencesViewModel = viewModel(
factory = PreferencesViewModel.PreferencesViewModelFactory(dataStore)
)
val fileViewModel: FileViewModel = viewModel()

val isActionViewOrEdit = (intent.action == Intent.ACTION_VIEW) or (intent.action == Intent.ACTION_EDIT)

Expand All @@ -53,6 +57,7 @@ class MainActivity : ComponentActivity() {
BeauTyXTApp(
modifier = Modifier,
fileViewModel = fileViewModel,
typstProjectViewModel = typstProjectViewModel,
preferencesViewModel = preferencesViewModel,
isActionViewOrEdit = isActionViewOrEdit,
)
Expand Down
112 changes: 90 additions & 22 deletions app/src/main/kotlin/dev/soupslurpr/beautyxt/ui/FileViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,34 +1,96 @@
package dev.soupslurpr.beautyxt.ui

import android.app.Application
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.database.Cursor
import android.net.Uri
import android.os.IBinder
import android.provider.DocumentsContract
import android.provider.OpenableColumns
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.viewModelScope
import dev.soupslurpr.beautyxt.IFileViewModelRustLibraryAidlInterface
import dev.soupslurpr.beautyxt.constants.mimeTypeMarkdown
import dev.soupslurpr.beautyxt.data.FileUiState
import dev.soupslurpr.beautyxt.markdownToDocx
import dev.soupslurpr.beautyxt.markdownToHtml
import dev.soupslurpr.beautyxt.plainTextToDocx
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.FileOutputStream
import java.io.InputStreamReader
import kotlin.coroutines.resume

class FileViewModel : ViewModel() {
class FileViewModel(application: Application) : AndroidViewModel(application) {

/**
* File state for this file
*/
private val _uiState = MutableStateFlow(FileUiState())
val uiState: StateFlow<FileUiState> = _uiState.asStateFlow()

private var rustService: MutableLiveData<IFileViewModelRustLibraryAidlInterface?> = MutableLiveData(null)

private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val rustService = IFileViewModelRustLibraryAidlInterface.Stub.asInterface(service)

this@FileViewModel.rustService.postValue(rustService)
}

override fun onServiceDisconnected(name: ComponentName?) {
rustService.postValue(null)
}
}

private val intentService = Intent(getApplication(), FileViewModelRustLibraryIsolatedService::class.java)

private suspend fun <T> LiveData<T>.awaitFirstNonNull(): T {
return withContext(Dispatchers.Main.immediate) {
suspendCancellableCoroutine { continuation ->
val observer = object : Observer<T> {
override fun onChanged(value: T) {
if (value != null) {
continuation.resume(value)
this@awaitFirstNonNull.removeObserver(this)
}
}
}

observeForever(observer)

// Handle coroutine cancellation
continuation.invokeOnCancellation {
this@awaitFirstNonNull.removeObserver(observer)
}
}
}
}

private fun bindService() {
getApplication<Application>().bindService(intentService, serviceConnection, Context.BIND_AUTO_CREATE)
}

private fun unbindService() {
getApplication<Application>().unbindService(serviceConnection)
}

init {
bindService()
}

/**
* Set the uri for this file and update the content
*/
Expand Down Expand Up @@ -147,7 +209,11 @@ class FileViewModel : ViewModel() {
}

fun setMarkdownToHtml() {
_uiState.value.contentConvertedToHtml.value = markdownToHtml(uiState.value.content.value)
viewModelScope.launch {
_uiState.value.contentConvertedToHtml.value = rustService.awaitFirstNonNull()!!.markdownToHtml(
uiState.value.content.value
)
}
}

fun setReadOnly(readOnly: Boolean) {
Expand All @@ -157,7 +223,6 @@ class FileViewModel : ViewModel() {
fun setWordCount() {
val wordCount = uiState.value.content.value.split("\\s+".toRegex()).filter { it.isNotEmpty() }.size.toLong()
_uiState.value.wordCount.value = wordCount

}

fun setCharacterCount() {
Expand Down Expand Up @@ -235,31 +300,34 @@ class FileViewModel : ViewModel() {
}

fun exportAsDocx(uri: Uri, context: Context) {
val docx = when (uiState.value.mimeType.value) {
mimeTypeMarkdown -> {
markdownToDocx(uiState.value.content.value)
}
viewModelScope.launch {
val docx = when (uiState.value.mimeType.value) {
mimeTypeMarkdown -> {
rustService.awaitFirstNonNull()!!.markdownToDocx(uiState.value.content.value)
}

else -> {
plainTextToDocx(uiState.value.content.value)
else -> {
rustService.awaitFirstNonNull()!!.plainTextToDocx(uiState.value.content.value)
}
}
}

try {
val contentResolver = context.contentResolver
contentResolver.openFileDescriptor(uri, "wt")?.use {
FileOutputStream(it.fileDescriptor).use {
it.write(docx)
try {
val contentResolver = context.contentResolver
contentResolver.openFileDescriptor(uri, "wt")?.use {
FileOutputStream(it.fileDescriptor).use {
it.write(docx)
}
}
}
} finally {
} finally {

}
// TODO: Handle exceptions
}
// TODO: Handle exceptions
}

override fun onCleared() {
super.onCleared()
clearUiState()
unbindService()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package dev.soupslurpr.beautyxt.ui

import android.app.Service
import android.content.Intent
import android.os.IBinder
import dev.soupslurpr.beautyxt.IFileViewModelRustLibraryAidlInterface

class FileViewModelRustLibraryIsolatedService : Service() {
private val binder = object : IFileViewModelRustLibraryAidlInterface.Stub() {
override fun markdownToHtml(markdown: String?): String {
return dev.soupslurpr.beautyxt.markdownToHtml(markdown!!)
}

override fun markdownToDocx(markdown: String?): ByteArray {
return dev.soupslurpr.beautyxt.markdownToDocx(markdown!!)
}

override fun plainTextToDocx(plainText: String?): ByteArray {
return dev.soupslurpr.beautyxt.plainTextToDocx(plainText!!)
}
}

override fun onBind(intent: Intent?): IBinder {
return binder
}
}

0 comments on commit de6f3dc

Please sign in to comment.