1
1
package app.alextran.immich
2
2
3
- import android.content.ContentResolver
4
- import android.content.ContentUris
5
- import android.content.ContentValues
6
3
import android.content.Context
7
- import android.content.Intent
8
- import android.net.Uri
9
- import android.os.Build
10
- import android.os.Bundle
11
- import android.os.Environment
12
- import android.provider.MediaStore
13
- import android.provider.Settings
14
4
import android.util.Log
15
5
import io.flutter.embedding.engine.plugins.FlutterPlugin
16
- import io.flutter.embedding.engine.plugins.activity.ActivityAware
17
- import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
18
6
import io.flutter.plugin.common.BinaryMessenger
19
7
import io.flutter.plugin.common.MethodCall
20
8
import io.flutter.plugin.common.MethodChannel
21
- import io.flutter.plugin.common.MethodChannel.Result
22
- import io.flutter.plugin.common.PluginRegistry
23
9
import java.security.MessageDigest
24
10
import java.io.FileInputStream
25
11
import kotlinx.coroutines.*
26
12
27
13
/* *
28
- * Android plugin for Dart `BackgroundService` and file trash operations
14
+ * Android plugin for Dart `BackgroundService`
15
+ *
16
+ * Receives messages/method calls from the foreground Dart side to manage
17
+ * the background service, e.g. start (enqueue), stop (cancel)
29
18
*/
30
- class BackgroundServicePlugin : FlutterPlugin , MethodChannel .MethodCallHandler , ActivityAware , PluginRegistry . ActivityResultListener {
19
+ class BackgroundServicePlugin : FlutterPlugin , MethodChannel .MethodCallHandler {
31
20
32
21
private var methodChannel: MethodChannel ? = null
33
- private var fileTrashChannel: MethodChannel ? = null
34
22
private var context: Context ? = null
35
- private var pendingResult: Result ? = null
36
- private val PERMISSION_REQUEST_CODE = 1001
37
- private var activityBinding: ActivityPluginBinding ? = null
38
23
39
24
override fun onAttachedToEngine (binding : FlutterPlugin .FlutterPluginBinding ) {
40
25
onAttachedToEngine(binding.applicationContext, binding.binaryMessenger)
@@ -44,10 +29,6 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
44
29
context = ctx
45
30
methodChannel = MethodChannel (messenger, " immich/foregroundChannel" )
46
31
methodChannel?.setMethodCallHandler(this )
47
-
48
- // Add file trash channel
49
- fileTrashChannel = MethodChannel (messenger, " file_trash" )
50
- fileTrashChannel?.setMethodCallHandler(this )
51
32
}
52
33
53
34
override fun onDetachedFromEngine (binding : FlutterPlugin .FlutterPluginBinding ) {
@@ -57,14 +38,11 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
57
38
private fun onDetachedFromEngine () {
58
39
methodChannel?.setMethodCallHandler(null )
59
40
methodChannel = null
60
- fileTrashChannel?.setMethodCallHandler(null )
61
- fileTrashChannel = null
62
41
}
63
42
64
- override fun onMethodCall (call : MethodCall , result : Result ) {
43
+ override fun onMethodCall (call : MethodCall , result : MethodChannel . Result ) {
65
44
val ctx = context!!
66
45
when (call.method) {
67
- // Existing BackgroundService methods
68
46
" enable" -> {
69
47
val args = call.arguments<ArrayList <* >>()!!
70
48
ctx.getSharedPreferences(BackupWorker .SHARED_PREF_NAME , Context .MODE_PRIVATE )
@@ -136,180 +114,10 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
136
114
}
137
115
}
138
116
139
- // File Trash methods moved from MainActivity
140
- " moveToTrash" -> {
141
- val fileName = call.argument<String >(" fileName" )
142
- if (fileName != null ) {
143
- if (hasManageStoragePermission()) {
144
- val success = moveToTrash(fileName)
145
- result.success(success)
146
- } else {
147
- result.error(" PERMISSION_DENIED" , " Storage permission required" , null )
148
- }
149
- } else {
150
- result.error(" INVALID_NAME" , " The file name is not specified." , null )
151
- }
152
- }
153
-
154
- " restoreFromTrash" -> {
155
- val fileName = call.argument<String >(" fileName" )
156
- if (fileName != null ) {
157
- if (hasManageStoragePermission()) {
158
- val success = untrashImage(fileName)
159
- result.success(success)
160
- } else {
161
- result.error(" PERMISSION_DENIED" , " Storage permission required" , null )
162
- }
163
- } else {
164
- result.error(" INVALID_NAME" , " The file name is not specified." , null )
165
- }
166
- }
167
-
168
- " requestManageStoragePermission" -> {
169
- if (! hasManageStoragePermission()) {
170
- requestManageStoragePermission(result)
171
- } else {
172
- Log .e(" Manage storage permission" , " Permission already granted" )
173
- result.success(true )
174
- }
175
- }
176
-
177
117
else -> result.notImplemented()
178
118
}
179
119
}
180
-
181
- // File Trash methods moved from MainActivity
182
- private fun hasManageStoragePermission (): Boolean {
183
- return if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .R ) {
184
- Environment .isExternalStorageManager()
185
- } else {
186
- true
187
- }
188
- }
189
-
190
- private fun requestManageStoragePermission (result : Result ) {
191
- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .R ) {
192
- pendingResult = result // Store the result callback
193
- val activity = activityBinding?.activity ? : return
194
-
195
- val intent = Intent (Settings .ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION )
196
- intent.data = Uri .parse(" package:${activity.packageName} " )
197
- activity.startActivityForResult(intent, PERMISSION_REQUEST_CODE )
198
- } else {
199
- result.success(true )
200
- }
201
- }
202
-
203
- private fun moveToTrash (fileName : String ): Boolean {
204
- val contentResolver = context?.contentResolver ? : return false
205
- val uri = getFileUri(fileName)
206
- Log .e(" FILE_URI" , uri.toString())
207
- return uri?.let { moveToTrash(it) } ? : false
208
- }
209
-
210
- private fun moveToTrash (contentUri : Uri ): Boolean {
211
- val contentResolver = context?.contentResolver ? : return false
212
- return try {
213
- val values = ContentValues ().apply {
214
- put(MediaStore .MediaColumns .IS_TRASHED , 1 ) // Move to trash
215
- }
216
- val updated = contentResolver.update(contentUri, values, null , null )
217
- updated > 0
218
- } catch (e: Exception ) {
219
- Log .e(" TrashError" , " Error moving to trash" , e)
220
- false
221
- }
222
- }
223
-
224
- private fun getFileUri (fileName : String ): Uri ? {
225
- val contentResolver = context?.contentResolver ? : return null
226
- val contentUri = MediaStore .Files .getContentUri(" external" )
227
- val projection = arrayOf(MediaStore .Images .Media ._ID )
228
- val selection = " ${MediaStore .Images .Media .DISPLAY_NAME } = ?"
229
- val selectionArgs = arrayOf(fileName)
230
- var fileUri: Uri ? = null
231
-
232
- contentResolver.query(contentUri, projection, selection, selectionArgs, null )?.use { cursor ->
233
- if (cursor.moveToFirst()) {
234
- val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore .Images .Media ._ID ))
235
- fileUri = ContentUris .withAppendedId(contentUri, id)
236
- }
237
- }
238
- return fileUri
239
- }
240
-
241
- private fun untrashImage (name : String ): Boolean {
242
- val contentResolver = context?.contentResolver ? : return false
243
- val uri = getTrashedFileUri(contentResolver, name)
244
- Log .e(" FILE_URI" , uri.toString())
245
- return uri?.let { untrashImage(it) } ? : false
246
- }
247
-
248
- private fun untrashImage (contentUri : Uri ): Boolean {
249
- val contentResolver = context?.contentResolver ? : return false
250
- return try {
251
- val values = ContentValues ().apply {
252
- put(MediaStore .MediaColumns .IS_TRASHED , 0 ) // Restore file
253
- }
254
- val updated = contentResolver.update(contentUri, values, null , null )
255
- updated > 0
256
- } catch (e: Exception ) {
257
- Log .e(" TrashError" , " Error restoring file" , e)
258
- false
259
- }
260
- }
261
-
262
- private fun getTrashedFileUri (contentResolver : ContentResolver , fileName : String ): Uri ? {
263
- val contentUri = MediaStore .Files .getContentUri(MediaStore .VOLUME_EXTERNAL )
264
- val projection = arrayOf(MediaStore .Files .FileColumns ._ID )
265
-
266
- val queryArgs = Bundle ().apply {
267
- putString(ContentResolver .QUERY_ARG_SQL_SELECTION , " ${MediaStore .Files .FileColumns .DISPLAY_NAME } = ?" )
268
- putStringArray(ContentResolver .QUERY_ARG_SQL_SELECTION_ARGS , arrayOf(fileName))
269
- putInt(MediaStore .QUERY_ARG_MATCH_TRASHED , MediaStore .MATCH_ONLY )
270
- }
271
-
272
- contentResolver.query(contentUri, projection, queryArgs, null )?.use { cursor ->
273
- if (cursor.moveToFirst()) {
274
- val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore .Files .FileColumns ._ID ))
275
- return ContentUris .withAppendedId(contentUri, id)
276
- }
277
- }
278
- return null
279
- }
280
-
281
- // ActivityAware implementation
282
- override fun onAttachedToActivity (binding : ActivityPluginBinding ) {
283
- activityBinding = binding
284
- binding.addActivityResultListener(this )
285
- }
286
-
287
- override fun onDetachedFromActivityForConfigChanges () {
288
- activityBinding?.removeActivityResultListener(this )
289
- activityBinding = null
290
- }
291
-
292
- override fun onReattachedToActivityForConfigChanges (binding : ActivityPluginBinding ) {
293
- activityBinding = binding
294
- binding.addActivityResultListener(this )
295
- }
296
-
297
- override fun onDetachedFromActivity () {
298
- activityBinding?.removeActivityResultListener(this )
299
- activityBinding = null
300
- }
301
-
302
- // ActivityResultListener implementation
303
- override fun onActivityResult (requestCode : Int , resultCode : Int , data : Intent ? ): Boolean {
304
- if (requestCode == PERMISSION_REQUEST_CODE ) {
305
- val granted = hasManageStoragePermission()
306
- pendingResult?.success(granted)
307
- pendingResult = null
308
- return true
309
- }
310
- return false
311
- }
312
120
}
313
121
314
122
private const val TAG = " BackgroundServicePlugin"
315
- private const val BUFFER_SIZE = 2 * 1024 * 1024
123
+ private const val BUFFER_SIZE = 2 * 1024 * 1024 ;
0 commit comments