Update
This commit is contained in:
parent
baac9e7fce
commit
91903e5282
7 changed files with 232 additions and 56 deletions
|
|
@ -7,6 +7,10 @@
|
||||||
<package android:name="net.kdt.pojavlaunch.debug" />
|
<package android:name="net.kdt.pojavlaunch.debug" />
|
||||||
</queries>
|
</queries>
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||||
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".PojavBackupApplication"
|
android:name=".PojavBackupApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
|
@ -39,6 +43,7 @@
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.startup.InitializationProvider"
|
android:name="androidx.startup.InitializationProvider"
|
||||||
android:authorities="${applicationId}.androidx-startup"
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
|
@ -50,6 +55,10 @@
|
||||||
android:value="androidx.startup"
|
android:value="androidx.startup"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
</provider>
|
</provider>
|
||||||
</application>
|
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
|
tools:node="merge" />
|
||||||
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
@ -1,21 +1,29 @@
|
||||||
package eu.m724.pojavbackup.core.backup
|
package eu.m724.pojavbackup.core.backup
|
||||||
|
|
||||||
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.hilt.work.HiltWorker
|
import androidx.hilt.work.HiltWorker
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.Data
|
import androidx.work.Data
|
||||||
|
import androidx.work.ForegroundInfo
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import eu.m724.pojavbackup.R
|
||||||
import eu.m724.pojavbackup.core.backup.Backup.BackupStatus
|
import eu.m724.pojavbackup.core.backup.Backup.BackupStatus
|
||||||
import eu.m724.pojavbackup.core.data.GameDataRepository
|
import eu.m724.pojavbackup.core.data.GameDataRepository
|
||||||
import eu.m724.pojavbackup.core.datastore.SettingsRepository
|
import eu.m724.pojavbackup.core.datastore.SettingsRepository
|
||||||
|
import eu.m724.pojavbackup.notification.NotificationChannels
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import org.apache.commons.compress.compressors.CompressorStreamFactory
|
import org.apache.commons.compress.compressors.CompressorStreamFactory
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
@HiltWorker
|
@HiltWorker
|
||||||
class BackupWorker @AssistedInject constructor(
|
class BackupWorker @AssistedInject constructor(
|
||||||
|
|
@ -25,30 +33,45 @@ class BackupWorker @AssistedInject constructor(
|
||||||
private val backupRepository: BackupRepository,
|
private val backupRepository: BackupRepository,
|
||||||
private val gameDataRepository: GameDataRepository
|
private val gameDataRepository: GameDataRepository
|
||||||
) : CoroutineWorker(appContext, workerParams) {
|
) : CoroutineWorker(appContext, workerParams) {
|
||||||
|
private val notificationManager =
|
||||||
|
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as
|
||||||
|
NotificationManager
|
||||||
|
|
||||||
|
private val notificationId: Int =
|
||||||
|
Random.nextInt()
|
||||||
|
|
||||||
|
private val errors = mutableListOf<Throwable>()
|
||||||
|
|
||||||
|
private lateinit var backup: Backup
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
setProgress(statusData(UUID(0, 0), "Initializing"))
|
try {
|
||||||
|
backup = backupRepository.createBackup()
|
||||||
|
|
||||||
val backup = backupRepository.createBackup()
|
updateStatus("Initializing")
|
||||||
|
|
||||||
val settings = settingsRepository.getSettings()
|
delay(10000)
|
||||||
val worldIds = settingsRepository.getIncludedWorldIds(settings.worldOrder)
|
|
||||||
|
|
||||||
// TODO multiple destinations support and remove those !!
|
val settings = settingsRepository.getSettings()
|
||||||
val backupUri = settings.destinationsList.first().uri.toUri()
|
val worldIds = settingsRepository.getIncludedWorldIds(settings.worldOrder)
|
||||||
val backupDirectory = DocumentFile.fromTreeUri(applicationContext, backupUri)!!
|
|
||||||
.createDirectory(DateTimeFormatter.ISO_LOCAL_DATE.format(LocalDate.now()) + " (" + backup.id + ")")!!
|
// TODO multiple destinations support and remove those !!
|
||||||
|
val backupUri = settings.destinationsList.first().uri.toUri()
|
||||||
|
val backupDirectory = DocumentFile.fromTreeUri(applicationContext, backupUri)!!
|
||||||
|
.createDirectory(DateTimeFormatter.ISO_LOCAL_DATE.format(LocalDate.now()) + " (" + backup.id + ")")!!
|
||||||
|
|
||||||
return try {
|
|
||||||
worldIds.forEach {
|
worldIds.forEach {
|
||||||
setProgress(statusData(backup.id, "$it: now backing up"))
|
updateStatus("Backing up world $it")
|
||||||
|
|
||||||
val world = runCatching {
|
val world = runCatching {
|
||||||
gameDataRepository.getWorld(it)
|
gameDataRepository.getWorld(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (world.isFailure) {
|
if (world.isFailure) {
|
||||||
Exception("Error backing up world $it", world.exceptionOrNull()).printStackTrace()
|
val e = Exception("Error backing up world $it", world.exceptionOrNull())
|
||||||
setProgress(statusData(backup.id, "$it: error"))
|
errors.add(e)
|
||||||
|
|
||||||
|
e.printStackTrace()
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,27 +88,71 @@ class BackupWorker @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val e = Exception("Error backing up", e)
|
||||||
|
errors.add(e)
|
||||||
|
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.isEmpty()) {
|
||||||
backupRepository.completeBackup(backup, BackupStatus.SUCCESS)
|
backupRepository.completeBackup(backup, BackupStatus.SUCCESS)
|
||||||
|
|
||||||
Result.success(statusData(backup.id, "Success"))
|
return Result.success(statusData("Backup completed successfully"))
|
||||||
} catch (e: Exception) {
|
} else {
|
||||||
Exception("Error backing up", e).printStackTrace()
|
|
||||||
|
|
||||||
backupRepository.completeBackup(backup, BackupStatus.FAILURE)
|
backupRepository.completeBackup(backup, BackupStatus.FAILURE)
|
||||||
|
|
||||||
Result.failure(statusData(backup.id, "Error: $e"))
|
// TODO notify and tell error
|
||||||
|
|
||||||
|
return Result.failure(statusData("Backup failed"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun statusData(uuid: UUID, status: String): Data {
|
fun statusData(status: String): Data {
|
||||||
return Data.Builder()
|
return Data.Builder()
|
||||||
.putUuid("uuid", uuid)
|
.putUuid("uuid", backup.id)
|
||||||
.putString("status", status)
|
.putString("status", status)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun updateStatus(status: String) {
|
||||||
|
setForeground(createForegroundInfo(status))
|
||||||
|
|
||||||
|
val data = statusData(status)
|
||||||
|
|
||||||
|
setProgress(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun finish(status: String, success: Boolean) {
|
||||||
|
val data = statusData(status)
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
Result.success(data)
|
||||||
|
} else {
|
||||||
|
Result.failure(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun Data.Builder.putUuid(key: String, value: UUID): Data.Builder {
|
fun Data.Builder.putUuid(key: String, value: UUID): Data.Builder {
|
||||||
return this.putLongArray(key, longArrayOf(value.mostSignificantBits, value.leastSignificantBits))
|
return this.putLongArray(key, longArrayOf(value.mostSignificantBits, value.leastSignificantBits))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Creates an instance of ForegroundInfo which can be used to update the
|
||||||
|
// ongoing notification.
|
||||||
|
private fun createForegroundInfo(progress: String): ForegroundInfo {
|
||||||
|
val title = "Backup in progress"
|
||||||
|
|
||||||
|
val notification = NotificationCompat.Builder(applicationContext, NotificationChannels.BACKUP_PROGRESS)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setTicker(title)
|
||||||
|
.setContentText(progress)
|
||||||
|
.setSmallIcon(R.drawable.baseline_history_24) // TODO icon
|
||||||
|
.setOngoing(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// TODO add action to pause and cancel
|
||||||
|
|
||||||
|
return ForegroundInfo(notificationId, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package eu.m724.pojavbackup.home
|
package eu.m724.pojavbackup.home
|
||||||
|
|
||||||
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
@ -10,6 +11,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import eu.m724.pojavbackup.core.data.GameDataRepository
|
import eu.m724.pojavbackup.core.data.GameDataRepository
|
||||||
import eu.m724.pojavbackup.core.datastore.SettingsRepository
|
import eu.m724.pojavbackup.core.datastore.SettingsRepository
|
||||||
|
import eu.m724.pojavbackup.notification.NotificationChannels
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
@ -28,6 +30,13 @@ class HomeViewModel @Inject constructor(
|
||||||
private val _uiState = MutableStateFlow(HomeUiState())
|
private val _uiState = MutableStateFlow(HomeUiState())
|
||||||
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
val notificationManager =
|
||||||
|
appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
|
NotificationChannels.register(notificationManager)
|
||||||
|
}
|
||||||
|
|
||||||
fun load(
|
fun load(
|
||||||
onSetupNeeded: () -> Unit
|
onSetupNeeded: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package eu.m724.pojavbackup.notification
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
|
||||||
|
object NotificationChannels {
|
||||||
|
const val BACKUP_PROGRESS = "BACKUP_PROGRESS"
|
||||||
|
const val BACKUP_ERROR = "BACKUP_ERROR"
|
||||||
|
|
||||||
|
fun register(notificationManager: NotificationManager) {
|
||||||
|
arrayOf(
|
||||||
|
NotificationChannel(BACKUP_PROGRESS, "Backup progress", NotificationManager.IMPORTANCE_LOW).apply {
|
||||||
|
description = "Appears when a backup is in progress and gives you status"
|
||||||
|
},
|
||||||
|
|
||||||
|
NotificationChannel(BACKUP_ERROR, "Backup error", NotificationManager.IMPORTANCE_DEFAULT).apply {
|
||||||
|
description = "Appears when a backup failed"
|
||||||
|
}
|
||||||
|
).forEach {
|
||||||
|
notificationManager.createNotificationChannel(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -51,7 +51,22 @@ class SetupActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val notificationGrant = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||||
|
if (isGranted) {
|
||||||
|
onComplete()
|
||||||
|
} else {
|
||||||
|
// TODO instead red text?
|
||||||
|
Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
"This is not a PojavLauncher directory.",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onComplete() {
|
fun onComplete() {
|
||||||
|
if (!viewModel.checkIfCanProceed()) return
|
||||||
|
|
||||||
startActivity(Intent(applicationContext, HomeActivity::class.java))
|
startActivity(Intent(applicationContext, HomeActivity::class.java))
|
||||||
finishActivity(0)
|
finishActivity(0)
|
||||||
}
|
}
|
||||||
|
|
@ -61,6 +76,11 @@ class SetupActivity : ComponentActivity() {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
val storagePermissionGranted = intent.getBooleanExtra("storagePermissionGranted", false)
|
||||||
|
val notificationPermissionGranted = intent.getBooleanExtra("notificationPermissionGranted", false)
|
||||||
|
|
||||||
|
viewModel.init(storagePermissionGranted, notificationPermissionGranted)
|
||||||
|
|
||||||
// println("Found pojav launchers: ${packages.joinToString(", ")}")
|
// println("Found pojav launchers: ${packages.joinToString(", ")}")
|
||||||
|
|
||||||
val defaultUri = "content://net.kdt.pojavlaunch.scoped.gamefolder.debug/tree/%2Fstorage%2Femulated%2F0%2FAndroid%2Fdata%2Fnet.kdt.pojavlaunch.debug%2Ffiles".toUri()
|
val defaultUri = "content://net.kdt.pojavlaunch.scoped.gamefolder.debug/tree/%2Fstorage%2Femulated%2F0%2FAndroid%2Fdata%2Fnet.kdt.pojavlaunch.debug%2Ffiles".toUri()
|
||||||
|
|
@ -71,7 +91,10 @@ class SetupActivity : ComponentActivity() {
|
||||||
SetupScreen(
|
SetupScreen(
|
||||||
modifier = Modifier.padding(innerPadding),
|
modifier = Modifier.padding(innerPadding),
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
onGrantClick = {
|
onStoragePermissionGrantClick = {
|
||||||
|
openDocumentTree.launch(defaultUri)
|
||||||
|
},
|
||||||
|
onNotificationPermissionGrantClick = {
|
||||||
openDocumentTree.launch(defaultUri)
|
openDocumentTree.launch(defaultUri)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -85,7 +108,8 @@ class SetupActivity : ComponentActivity() {
|
||||||
fun SetupScreen(
|
fun SetupScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: SetupViewModel,
|
viewModel: SetupViewModel,
|
||||||
onGrantClick: () -> Unit
|
onStoragePermissionGrantClick: () -> Unit,
|
||||||
|
onNotificationPermissionGrantClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
|
@ -100,41 +124,65 @@ fun SetupScreen(
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Card(
|
GrantCard(
|
||||||
elevation =
|
title = "Storage permission",
|
||||||
if (uiState.storagePermissionGranted) {
|
description = "It's needed to access your saves etc.",
|
||||||
CardDefaults.cardElevation()
|
granted = uiState.storagePermissionGranted,
|
||||||
} else {
|
onClick = onStoragePermissionGrantClick
|
||||||
CardDefaults.elevatedCardElevation()
|
)
|
||||||
}
|
|
||||||
|
GrantCard(
|
||||||
|
title = "Notification permission",
|
||||||
|
description = "It's needed to notify you about backup status.",
|
||||||
|
granted = uiState.storagePermissionGranted,
|
||||||
|
onClick = onNotificationPermissionGrantClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GrantCard(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
granted: Boolean,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier,
|
||||||
|
elevation =
|
||||||
|
if (granted) {
|
||||||
|
CardDefaults.cardElevation()
|
||||||
|
} else {
|
||||||
|
CardDefaults.elevatedCardElevation()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.width(300.dp).padding(
|
||||||
|
horizontal = 20.dp,
|
||||||
|
vertical = 10.dp
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Column(
|
Text(
|
||||||
modifier = Modifier.width(300.dp).padding(
|
text = title,
|
||||||
horizontal = 20.dp,
|
fontWeight = FontWeight.Bold,
|
||||||
vertical = 10.dp
|
textAlign = TextAlign.Center,
|
||||||
)
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = description
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
|
enabled = !granted
|
||||||
) {
|
) {
|
||||||
Text(
|
if (granted) {
|
||||||
text = "Storage permission",
|
Text("Already granted")
|
||||||
fontWeight = FontWeight.Bold,
|
} else {
|
||||||
textAlign = TextAlign.Center,
|
Text("Grant")
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "It's needed to access your saves etc."
|
|
||||||
)
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = onGrantClick,
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
|
||||||
enabled = !uiState.storagePermissionGranted
|
|
||||||
) {
|
|
||||||
if (uiState.storagePermissionGranted) {
|
|
||||||
Text("Already granted")
|
|
||||||
} else {
|
|
||||||
Text("Grant")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package eu.m724.pojavbackup.setup
|
package eu.m724.pojavbackup.setup
|
||||||
|
|
||||||
data class SetupUiState(
|
data class SetupUiState(
|
||||||
val storagePermissionGranted: Boolean = false
|
val storagePermissionGranted: Boolean = false,
|
||||||
|
val notificationPermissionGranted: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
@ -32,6 +32,24 @@ class SetupViewModel @Inject constructor(
|
||||||
private val _uiState = MutableStateFlow(SetupUiState())
|
private val _uiState = MutableStateFlow(SetupUiState())
|
||||||
val uiState: StateFlow<SetupUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<SetupUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
fun init(
|
||||||
|
storagePermissionGranted: Boolean,
|
||||||
|
notificationPermissionGranted: Boolean
|
||||||
|
) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
storagePermissionGranted = storagePermissionGranted,
|
||||||
|
notificationPermissionGranted = notificationPermissionGranted
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkIfCanProceed(): Boolean {
|
||||||
|
val state = _uiState.value
|
||||||
|
|
||||||
|
return state.storagePermissionGranted && state.notificationPermissionGranted
|
||||||
|
}
|
||||||
|
|
||||||
// TODO we could make the check call separate and not pass context here
|
// TODO we could make the check call separate and not pass context here
|
||||||
fun onOpenDocumentTree(context: Context, uri: Uri?, result: (Boolean) -> Unit) {
|
fun onOpenDocumentTree(context: Context, uri: Uri?, result: (Boolean) -> Unit) {
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue