Skip to content

Commit 61c3f27

Browse files
feat: add configurable backup on charging only and delay settings for android (#22114)
* feat: add configurable on charging only and delay * Segmented and style the settings --------- Co-authored-by: shenlong-tanwen <[email protected]> Co-authored-by: Alex <[email protected]>
1 parent b2ca208 commit 61c3f27

File tree

10 files changed

+585
-52
lines changed

10 files changed

+585
-52
lines changed

i18n/en.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,13 +533,15 @@
533533
"background_backup_running_error": "Background backup is currently running, cannot start manual backup",
534534
"background_location_permission": "Background location permission",
535535
"background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
536+
"background_options": "Background Options",
536537
"backup": "Backup",
537538
"backup_album_selection_page_albums_device": "Albums on device ({count})",
538539
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
539540
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
540541
"backup_album_selection_page_select_albums": "Select albums",
541542
"backup_album_selection_page_selection_info": "Selection Info",
542543
"backup_album_selection_page_total_assets": "Total unique assets",
544+
"backup_albums_sync": "Backup albums synchronization",
543545
"backup_all": "All",
544546
"backup_background_service_backup_failed_message": "Failed to backup assets. Retrying…",
545547
"backup_background_service_connection_failed_message": "Failed to connect to the server. Retrying…",
@@ -656,6 +658,8 @@
656658
"change_pin_code": "Change PIN code",
657659
"change_your_password": "Change your password",
658660
"changed_visibility_successfully": "Changed visibility successfully",
661+
"charging": "Charging",
662+
"charging_requirement_mobile_backup": "Background backup requires the device to be charging",
659663
"check_corrupt_asset_backup": "Check for corrupt asset backups",
660664
"check_corrupt_asset_backup_button": "Perform check",
661665
"check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
@@ -1351,6 +1355,7 @@
13511355
"name_or_nickname": "Name or nickname",
13521356
"network_requirement_photos_upload": "Use cellular data to backup photos",
13531357
"network_requirement_videos_upload": "Use cellular data to backup videos",
1358+
"network_requirements": "Network Requirements",
13541359
"network_requirements_updated": "Network requirements changed, resetting backup queue",
13551360
"networking_settings": "Networking",
13561361
"networking_subtitle": "Manage the server endpoint settings",

mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,36 @@ private object BackgroundWorkerPigeonUtils {
3737
)
3838
}
3939
}
40+
fun deepEquals(a: Any?, b: Any?): Boolean {
41+
if (a is ByteArray && b is ByteArray) {
42+
return a.contentEquals(b)
43+
}
44+
if (a is IntArray && b is IntArray) {
45+
return a.contentEquals(b)
46+
}
47+
if (a is LongArray && b is LongArray) {
48+
return a.contentEquals(b)
49+
}
50+
if (a is DoubleArray && b is DoubleArray) {
51+
return a.contentEquals(b)
52+
}
53+
if (a is Array<*> && b is Array<*>) {
54+
return a.size == b.size &&
55+
a.indices.all{ deepEquals(a[it], b[it]) }
56+
}
57+
if (a is List<*> && b is List<*>) {
58+
return a.size == b.size &&
59+
a.indices.all{ deepEquals(a[it], b[it]) }
60+
}
61+
if (a is Map<*, *> && b is Map<*, *>) {
62+
return a.size == b.size && a.all {
63+
(b as Map<Any?, Any?>).containsKey(it.key) &&
64+
deepEquals(it.value, b[it.key])
65+
}
66+
}
67+
return a == b
68+
}
69+
4070
}
4171

4272
/**
@@ -50,18 +80,63 @@ class FlutterError (
5080
override val message: String? = null,
5181
val details: Any? = null
5282
) : Throwable()
83+
84+
/** Generated class from Pigeon that represents data sent in messages. */
85+
data class BackgroundWorkerSettings (
86+
val requiresCharging: Boolean,
87+
val minimumDelaySeconds: Long
88+
)
89+
{
90+
companion object {
91+
fun fromList(pigeonVar_list: List<Any?>): BackgroundWorkerSettings {
92+
val requiresCharging = pigeonVar_list[0] as Boolean
93+
val minimumDelaySeconds = pigeonVar_list[1] as Long
94+
return BackgroundWorkerSettings(requiresCharging, minimumDelaySeconds)
95+
}
96+
}
97+
fun toList(): List<Any?> {
98+
return listOf(
99+
requiresCharging,
100+
minimumDelaySeconds,
101+
)
102+
}
103+
override fun equals(other: Any?): Boolean {
104+
if (other !is BackgroundWorkerSettings) {
105+
return false
106+
}
107+
if (this === other) {
108+
return true
109+
}
110+
return BackgroundWorkerPigeonUtils.deepEquals(toList(), other.toList()) }
111+
112+
override fun hashCode(): Int = toList().hashCode()
113+
}
53114
private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() {
54115
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
55-
return super.readValueOfType(type, buffer)
116+
return when (type) {
117+
129.toByte() -> {
118+
return (readValue(buffer) as? List<Any?>)?.let {
119+
BackgroundWorkerSettings.fromList(it)
120+
}
121+
}
122+
else -> super.readValueOfType(type, buffer)
123+
}
56124
}
57125
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
58-
super.writeValue(stream, value)
126+
when (value) {
127+
is BackgroundWorkerSettings -> {
128+
stream.write(129)
129+
writeValue(stream, value.toList())
130+
}
131+
else -> super.writeValue(stream, value)
132+
}
59133
}
60134
}
61135

62136
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
63137
interface BackgroundWorkerFgHostApi {
64138
fun enable()
139+
fun configure(settings: BackgroundWorkerSettings)
65140
fun disable()
66141

67142
companion object {
@@ -89,6 +164,24 @@ interface BackgroundWorkerFgHostApi {
89164
channel.setMessageHandler(null)
90165
}
91166
}
167+
run {
168+
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.configure$separatedMessageChannelSuffix", codec)
169+
if (api != null) {
170+
channel.setMessageHandler { message, reply ->
171+
val args = message as List<Any?>
172+
val settingsArg = args[0] as BackgroundWorkerSettings
173+
val wrapped: List<Any?> = try {
174+
api.configure(settingsArg)
175+
listOf(null)
176+
} catch (exception: Throwable) {
177+
BackgroundWorkerPigeonUtils.wrapError(exception)
178+
}
179+
reply.reply(wrapped)
180+
}
181+
} else {
182+
channel.setMessageHandler(null)
183+
}
184+
}
92185
run {
93186
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$separatedMessageChannelSuffix", codec)
94187
if (api != null) {

mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerApiImpl.kt

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package app.alextran.immich.background
22

33
import android.content.Context
4+
import android.content.SharedPreferences
45
import android.provider.MediaStore
56
import android.util.Log
67
import androidx.work.BackoffPolicy
@@ -10,7 +11,7 @@ import androidx.work.OneTimeWorkRequest
1011
import androidx.work.WorkManager
1112
import java.util.concurrent.TimeUnit
1213

13-
private const val TAG = "BackgroundUploadImpl"
14+
private const val TAG = "BackgroundWorkerApiImpl"
1415

1516
class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
1617
private val ctx: Context = context.applicationContext
@@ -19,9 +20,16 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
1920
enqueueMediaObserver(ctx)
2021
}
2122

23+
override fun configure(settings: BackgroundWorkerSettings) {
24+
BackgroundWorkerPreferences(ctx).updateSettings(settings)
25+
enqueueMediaObserver(ctx)
26+
}
27+
2228
override fun disable() {
23-
WorkManager.getInstance(ctx).cancelUniqueWork(OBSERVER_WORKER_NAME)
24-
WorkManager.getInstance(ctx).cancelUniqueWork(BACKGROUND_WORKER_NAME)
29+
WorkManager.getInstance(ctx).apply {
30+
cancelUniqueWork(OBSERVER_WORKER_NAME)
31+
cancelUniqueWork(BACKGROUND_WORKER_NAME)
32+
}
2533
Log.i(TAG, "Cancelled background upload tasks")
2634
}
2735

@@ -30,22 +38,27 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
3038
private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1"
3139

3240
fun enqueueMediaObserver(ctx: Context) {
33-
val constraints = Constraints.Builder()
34-
.addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
35-
.addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
36-
.addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
37-
.addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
38-
.setTriggerContentUpdateDelay(30, TimeUnit.SECONDS)
39-
.setTriggerContentMaxDelay(3, TimeUnit.MINUTES)
40-
.build()
41+
val settings = BackgroundWorkerPreferences(ctx).getSettings()
42+
val constraints = Constraints.Builder().apply {
43+
addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
44+
addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
45+
addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
46+
addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
47+
setTriggerContentUpdateDelay(settings.minimumDelaySeconds, TimeUnit.SECONDS)
48+
setTriggerContentMaxDelay(settings.minimumDelaySeconds * 10, TimeUnit.MINUTES)
49+
setRequiresCharging(settings.requiresCharging)
50+
}.build()
4151

4252
val work = OneTimeWorkRequest.Builder(MediaObserver::class.java)
4353
.setConstraints(constraints)
4454
.build()
4555
WorkManager.getInstance(ctx)
4656
.enqueueUniqueWork(OBSERVER_WORKER_NAME, ExistingWorkPolicy.REPLACE, work)
4757

48-
Log.i(TAG, "Enqueued media observer worker with name: $OBSERVER_WORKER_NAME")
58+
Log.i(
59+
TAG,
60+
"Enqueued media observer worker with name: $OBSERVER_WORKER_NAME and settings: $settings"
61+
)
4962
}
5063

5164
fun enqueueBackgroundWorker(ctx: Context) {
@@ -62,3 +75,33 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
6275
}
6376
}
6477
}
78+
79+
private class BackgroundWorkerPreferences(private val ctx: Context) {
80+
companion object {
81+
private const val SHARED_PREF_NAME = "Immich::BackgroundWorker"
82+
private const val SHARED_PREF_MIN_DELAY_KEY = "BackgroundWorker::minDelaySeconds"
83+
private const val SHARED_PREF_REQUIRE_CHARGING_KEY = "BackgroundWorker::requireCharging"
84+
85+
private const val DEFAULT_MIN_DELAY_SECONDS = 30L
86+
private const val DEFAULT_REQUIRE_CHARGING = false
87+
}
88+
89+
private val sp: SharedPreferences by lazy {
90+
ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
91+
}
92+
93+
fun updateSettings(settings: BackgroundWorkerSettings) {
94+
sp.edit().apply {
95+
putLong(SHARED_PREF_MIN_DELAY_KEY, settings.minimumDelaySeconds)
96+
putBoolean(SHARED_PREF_REQUIRE_CHARGING_KEY, settings.requiresCharging)
97+
apply()
98+
}
99+
}
100+
101+
fun getSettings(): BackgroundWorkerSettings {
102+
return BackgroundWorkerSettings(
103+
minimumDelaySeconds = sp.getLong(SHARED_PREF_MIN_DELAY_KEY, DEFAULT_MIN_DELAY_SECONDS),
104+
requiresCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING_KEY, DEFAULT_REQUIRE_CHARGING),
105+
)
106+
}
107+
}

0 commit comments

Comments
 (0)