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" />
|
||||
</queries>
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||
|
||||
|
||||
<application
|
||||
android:name=".PojavBackupApplication"
|
||||
android:allowBackup="true"
|
||||
|
@ -39,6 +43,7 @@
|
|||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
|
@ -50,6 +55,10 @@
|
|||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
tools:node="merge" />
|
||||
</application>
|
||||
</manifest>
|
|
@ -1,21 +1,29 @@
|
|||
package eu.m724.pojavbackup.core.backup
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import eu.m724.pojavbackup.R
|
||||
import eu.m724.pojavbackup.core.backup.Backup.BackupStatus
|
||||
import eu.m724.pojavbackup.core.data.GameDataRepository
|
||||
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 java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.UUID
|
||||
import kotlin.random.Random
|
||||
|
||||
@HiltWorker
|
||||
class BackupWorker @AssistedInject constructor(
|
||||
|
@ -25,30 +33,45 @@ class BackupWorker @AssistedInject constructor(
|
|||
private val backupRepository: BackupRepository,
|
||||
private val gameDataRepository: GameDataRepository
|
||||
) : 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 {
|
||||
setProgress(statusData(UUID(0, 0), "Initializing"))
|
||||
try {
|
||||
backup = backupRepository.createBackup()
|
||||
|
||||
val backup = backupRepository.createBackup()
|
||||
updateStatus("Initializing")
|
||||
|
||||
val settings = settingsRepository.getSettings()
|
||||
val worldIds = settingsRepository.getIncludedWorldIds(settings.worldOrder)
|
||||
delay(10000)
|
||||
|
||||
// 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 + ")")!!
|
||||
val settings = settingsRepository.getSettings()
|
||||
val worldIds = settingsRepository.getIncludedWorldIds(settings.worldOrder)
|
||||
|
||||
// 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 {
|
||||
setProgress(statusData(backup.id, "$it: now backing up"))
|
||||
updateStatus("Backing up world $it")
|
||||
|
||||
val world = runCatching {
|
||||
gameDataRepository.getWorld(it)
|
||||
}
|
||||
|
||||
if (world.isFailure) {
|
||||
Exception("Error backing up world $it", world.exceptionOrNull()).printStackTrace()
|
||||
setProgress(statusData(backup.id, "$it: error"))
|
||||
val e = Exception("Error backing up world $it", world.exceptionOrNull())
|
||||
errors.add(e)
|
||||
|
||||
e.printStackTrace()
|
||||
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)
|
||||
|
||||
Result.success(statusData(backup.id, "Success"))
|
||||
} catch (e: Exception) {
|
||||
Exception("Error backing up", e).printStackTrace()
|
||||
|
||||
return Result.success(statusData("Backup completed successfully"))
|
||||
} else {
|
||||
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()
|
||||
.putUuid("uuid", uuid)
|
||||
.putUuid("uuid", backup.id)
|
||||
.putString("status", status)
|
||||
.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 {
|
||||
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
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
|
@ -10,6 +11,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.m724.pojavbackup.core.data.GameDataRepository
|
||||
import eu.m724.pojavbackup.core.datastore.SettingsRepository
|
||||
import eu.m724.pojavbackup.notification.NotificationChannels
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
@ -28,6 +30,13 @@ class HomeViewModel @Inject constructor(
|
|||
private val _uiState = MutableStateFlow(HomeUiState())
|
||||
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
val notificationManager =
|
||||
appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
NotificationChannels.register(notificationManager)
|
||||
}
|
||||
|
||||
fun load(
|
||||
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() {
|
||||
if (!viewModel.checkIfCanProceed()) return
|
||||
|
||||
startActivity(Intent(applicationContext, HomeActivity::class.java))
|
||||
finishActivity(0)
|
||||
}
|
||||
|
@ -61,6 +76,11 @@ class SetupActivity : ComponentActivity() {
|
|||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
val storagePermissionGranted = intent.getBooleanExtra("storagePermissionGranted", false)
|
||||
val notificationPermissionGranted = intent.getBooleanExtra("notificationPermissionGranted", false)
|
||||
|
||||
viewModel.init(storagePermissionGranted, notificationPermissionGranted)
|
||||
|
||||
// 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()
|
||||
|
@ -71,7 +91,10 @@ class SetupActivity : ComponentActivity() {
|
|||
SetupScreen(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
viewModel = viewModel,
|
||||
onGrantClick = {
|
||||
onStoragePermissionGrantClick = {
|
||||
openDocumentTree.launch(defaultUri)
|
||||
},
|
||||
onNotificationPermissionGrantClick = {
|
||||
openDocumentTree.launch(defaultUri)
|
||||
}
|
||||
)
|
||||
|
@ -85,7 +108,8 @@ class SetupActivity : ComponentActivity() {
|
|||
fun SetupScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: SetupViewModel,
|
||||
onGrantClick: () -> Unit
|
||||
onStoragePermissionGrantClick: () -> Unit,
|
||||
onNotificationPermissionGrantClick: () -> Unit
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
|
@ -100,41 +124,65 @@ fun SetupScreen(
|
|||
|
||||
)
|
||||
|
||||
Card(
|
||||
elevation =
|
||||
if (uiState.storagePermissionGranted) {
|
||||
CardDefaults.cardElevation()
|
||||
} else {
|
||||
CardDefaults.elevatedCardElevation()
|
||||
}
|
||||
GrantCard(
|
||||
title = "Storage permission",
|
||||
description = "It's needed to access your saves etc.",
|
||||
granted = uiState.storagePermissionGranted,
|
||||
onClick = onStoragePermissionGrantClick
|
||||
)
|
||||
|
||||
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(
|
||||
modifier = Modifier.width(300.dp).padding(
|
||||
horizontal = 20.dp,
|
||||
vertical = 10.dp
|
||||
)
|
||||
Text(
|
||||
text = title,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Text(
|
||||
text = description
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
enabled = !granted
|
||||
) {
|
||||
Text(
|
||||
text = "Storage permission",
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
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")
|
||||
}
|
||||
if (granted) {
|
||||
Text("Already granted")
|
||||
} else {
|
||||
Text("Grant")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package eu.m724.pojavbackup.setup
|
||||
|
||||
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())
|
||||
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
|
||||
fun onOpenDocumentTree(context: Context, uri: Uri?, result: (Boolean) -> Unit) {
|
||||
if (uri != null) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue