diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b773e42..2b0e5ee 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,6 +7,10 @@
+
+
+
+
+
-
+
+
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/core/backup/BackupWorker.kt b/app/src/main/java/eu/m724/pojavbackup/core/backup/BackupWorker.kt
index da7f06f..965acf1 100644
--- a/app/src/main/java/eu/m724/pojavbackup/core/backup/BackupWorker.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/core/backup/BackupWorker.kt
@@ -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()
+
+ 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)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/home/HomeViewModel.kt b/app/src/main/java/eu/m724/pojavbackup/home/HomeViewModel.kt
index 99e0b76..38b2afc 100644
--- a/app/src/main/java/eu/m724/pojavbackup/home/HomeViewModel.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/home/HomeViewModel.kt
@@ -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 = _uiState.asStateFlow()
+ init {
+ val notificationManager =
+ appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ NotificationChannels.register(notificationManager)
+ }
+
fun load(
onSetupNeeded: () -> Unit
) {
diff --git a/app/src/main/java/eu/m724/pojavbackup/notification/NotificationChannels.kt b/app/src/main/java/eu/m724/pojavbackup/notification/NotificationChannels.kt
new file mode 100644
index 0000000..517743b
--- /dev/null
+++ b/app/src/main/java/eu/m724/pojavbackup/notification/NotificationChannels.kt
@@ -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)
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/setup/SetupActivity.kt b/app/src/main/java/eu/m724/pojavbackup/setup/SetupActivity.kt
index f50b8d8..2e23632 100644
--- a/app/src/main/java/eu/m724/pojavbackup/setup/SetupActivity.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/setup/SetupActivity.kt
@@ -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")
}
}
}
diff --git a/app/src/main/java/eu/m724/pojavbackup/setup/SetupUiState.kt b/app/src/main/java/eu/m724/pojavbackup/setup/SetupUiState.kt
index 2804b3a..00b36e2 100644
--- a/app/src/main/java/eu/m724/pojavbackup/setup/SetupUiState.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/setup/SetupUiState.kt
@@ -1,5 +1,6 @@
package eu.m724.pojavbackup.setup
data class SetupUiState(
- val storagePermissionGranted: Boolean = false
+ val storagePermissionGranted: Boolean = false,
+ val notificationPermissionGranted: Boolean = false
)
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/pojavbackup/setup/SetupViewModel.kt b/app/src/main/java/eu/m724/pojavbackup/setup/SetupViewModel.kt
index 56fd903..c38d32e 100644
--- a/app/src/main/java/eu/m724/pojavbackup/setup/SetupViewModel.kt
+++ b/app/src/main/java/eu/m724/pojavbackup/setup/SetupViewModel.kt
@@ -32,6 +32,24 @@ class SetupViewModel @Inject constructor(
private val _uiState = MutableStateFlow(SetupUiState())
val uiState: StateFlow = _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) {