11package app.alextran.immich
22
3+ import android.content.ContentResolver
4+ import android.content.ContentUris
5+ import android.content.ContentValues
36import 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
414import android.util.Log
515import io.flutter.embedding.engine.plugins.FlutterPlugin
16+ import io.flutter.embedding.engine.plugins.activity.ActivityAware
17+ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
618import io.flutter.plugin.common.BinaryMessenger
719import io.flutter.plugin.common.MethodCall
820import io.flutter.plugin.common.MethodChannel
21+ import io.flutter.plugin.common.MethodChannel.Result
22+ import io.flutter.plugin.common.PluginRegistry
923import java.security.MessageDigest
1024import java.io.FileInputStream
1125import kotlinx.coroutines.*
1226
1327/* *
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)
28+ * Android plugin for Dart `BackgroundService` and file trash operations
1829 */
19- class BackgroundServicePlugin : FlutterPlugin , MethodChannel .MethodCallHandler {
30+ class BackgroundServicePlugin : FlutterPlugin , MethodChannel .MethodCallHandler , ActivityAware , PluginRegistry . ActivityResultListener {
2031
2132 private var methodChannel: MethodChannel ? = null
33+ private var fileTrashChannel: MethodChannel ? = null
2234 private var context: Context ? = null
35+ private var pendingResult: Result ? = null
36+ private val PERMISSION_REQUEST_CODE = 1001
37+ private var activityBinding: ActivityPluginBinding ? = null
2338
2439 override fun onAttachedToEngine (binding : FlutterPlugin .FlutterPluginBinding ) {
2540 onAttachedToEngine(binding.applicationContext, binding.binaryMessenger)
@@ -29,6 +44,10 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
2944 context = ctx
3045 methodChannel = MethodChannel (messenger, " immich/foregroundChannel" )
3146 methodChannel?.setMethodCallHandler(this )
47+
48+ // Add file trash channel
49+ fileTrashChannel = MethodChannel (messenger, " file_trash" )
50+ fileTrashChannel?.setMethodCallHandler(this )
3251 }
3352
3453 override fun onDetachedFromEngine (binding : FlutterPlugin .FlutterPluginBinding ) {
@@ -38,11 +57,14 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
3857 private fun onDetachedFromEngine () {
3958 methodChannel?.setMethodCallHandler(null )
4059 methodChannel = null
60+ fileTrashChannel?.setMethodCallHandler(null )
61+ fileTrashChannel = null
4162 }
4263
43- override fun onMethodCall (call : MethodCall , result : MethodChannel . Result ) {
64+ override fun onMethodCall (call : MethodCall , result : Result ) {
4465 val ctx = context!!
4566 when (call.method) {
67+ // Existing BackgroundService methods
4668 " enable" -> {
4769 val args = call.arguments<ArrayList <* >>()!!
4870 ctx.getSharedPreferences(BackupWorker .SHARED_PREF_NAME , Context .MODE_PRIVATE )
@@ -114,10 +136,180 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
114136 }
115137 }
116138
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+
117177 else -> result.notImplemented()
118178 }
119179 }
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+ }
120312}
121313
122314private const val TAG = " BackgroundServicePlugin"
123- private const val BUFFER_SIZE = 2 * 1024 * 1024 ;
315+ private const val BUFFER_SIZE = 2 * 1024 * 1024
0 commit comments