Skip to content

chore: revert #16732 #17819

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions mobile/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission android:name="android.permission.MANAGE_MEDIA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
Expand Down Expand Up @@ -125,4 +124,4 @@
<data android:scheme="geo" />
</intent>
</queries>
</manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,40 +1,25 @@
package app.alextran.immich

import android.content.ContentResolver
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.provider.Settings
import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry
import java.security.MessageDigest
import java.io.FileInputStream
import kotlinx.coroutines.*

/**
* Android plugin for Dart `BackgroundService` and file trash operations
* Android plugin for Dart `BackgroundService`
*
* Receives messages/method calls from the foreground Dart side to manage
* the background service, e.g. start (enqueue), stop (cancel)
*/
class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener {
class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {

private var methodChannel: MethodChannel? = null
private var fileTrashChannel: MethodChannel? = null
private var context: Context? = null
private var pendingResult: Result? = null
private val PERMISSION_REQUEST_CODE = 1001
private var activityBinding: ActivityPluginBinding? = null

override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
onAttachedToEngine(binding.applicationContext, binding.binaryMessenger)
Expand All @@ -44,10 +29,6 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
context = ctx
methodChannel = MethodChannel(messenger, "immich/foregroundChannel")
methodChannel?.setMethodCallHandler(this)

// Add file trash channel
fileTrashChannel = MethodChannel(messenger, "file_trash")
fileTrashChannel?.setMethodCallHandler(this)
}

override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
Expand All @@ -57,14 +38,11 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
private fun onDetachedFromEngine() {
methodChannel?.setMethodCallHandler(null)
methodChannel = null
fileTrashChannel?.setMethodCallHandler(null)
fileTrashChannel = null
}

override fun onMethodCall(call: MethodCall, result: Result) {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
val ctx = context!!
when (call.method) {
// Existing BackgroundService methods
"enable" -> {
val args = call.arguments<ArrayList<*>>()!!
ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
Expand Down Expand Up @@ -136,180 +114,10 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
}
}

// File Trash methods moved from MainActivity
"moveToTrash" -> {
val fileName = call.argument<String>("fileName")
if (fileName != null) {
if (hasManageStoragePermission()) {
val success = moveToTrash(fileName)
result.success(success)
} else {
result.error("PERMISSION_DENIED", "Storage permission required", null)
}
} else {
result.error("INVALID_NAME", "The file name is not specified.", null)
}
}

"restoreFromTrash" -> {
val fileName = call.argument<String>("fileName")
if (fileName != null) {
if (hasManageStoragePermission()) {
val success = untrashImage(fileName)
result.success(success)
} else {
result.error("PERMISSION_DENIED", "Storage permission required", null)
}
} else {
result.error("INVALID_NAME", "The file name is not specified.", null)
}
}

"requestManageStoragePermission" -> {
if (!hasManageStoragePermission()) {
requestManageStoragePermission(result)
} else {
Log.e("Manage storage permission", "Permission already granted")
result.success(true)
}
}

else -> result.notImplemented()
}
}

// File Trash methods moved from MainActivity
private fun hasManageStoragePermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Environment.isExternalStorageManager()
} else {
true
}
}

private fun requestManageStoragePermission(result: Result) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
pendingResult = result // Store the result callback
val activity = activityBinding?.activity ?: return

val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
intent.data = Uri.parse("package:${activity.packageName}")
activity.startActivityForResult(intent, PERMISSION_REQUEST_CODE)
} else {
result.success(true)
}
}

private fun moveToTrash(fileName: String): Boolean {
val contentResolver = context?.contentResolver ?: return false
val uri = getFileUri(fileName)
Log.e("FILE_URI", uri.toString())
return uri?.let { moveToTrash(it) } ?: false
}

private fun moveToTrash(contentUri: Uri): Boolean {
val contentResolver = context?.contentResolver ?: return false
return try {
val values = ContentValues().apply {
put(MediaStore.MediaColumns.IS_TRASHED, 1) // Move to trash
}
val updated = contentResolver.update(contentUri, values, null, null)
updated > 0
} catch (e: Exception) {
Log.e("TrashError", "Error moving to trash", e)
false
}
}

private fun getFileUri(fileName: String): Uri? {
val contentResolver = context?.contentResolver ?: return null
val contentUri = MediaStore.Files.getContentUri("external")
val projection = arrayOf(MediaStore.Images.Media._ID)
val selection = "${MediaStore.Images.Media.DISPLAY_NAME} = ?"
val selectionArgs = arrayOf(fileName)
var fileUri: Uri? = null

contentResolver.query(contentUri, projection, selection, selectionArgs, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID))
fileUri = ContentUris.withAppendedId(contentUri, id)
}
}
return fileUri
}

private fun untrashImage(name: String): Boolean {
val contentResolver = context?.contentResolver ?: return false
val uri = getTrashedFileUri(contentResolver, name)
Log.e("FILE_URI", uri.toString())
return uri?.let { untrashImage(it) } ?: false
}

private fun untrashImage(contentUri: Uri): Boolean {
val contentResolver = context?.contentResolver ?: return false
return try {
val values = ContentValues().apply {
put(MediaStore.MediaColumns.IS_TRASHED, 0) // Restore file
}
val updated = contentResolver.update(contentUri, values, null, null)
updated > 0
} catch (e: Exception) {
Log.e("TrashError", "Error restoring file", e)
false
}
}

private fun getTrashedFileUri(contentResolver: ContentResolver, fileName: String): Uri? {
val contentUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
val projection = arrayOf(MediaStore.Files.FileColumns._ID)

val queryArgs = Bundle().apply {
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?")
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(fileName))
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
}

contentResolver.query(contentUri, projection, queryArgs, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
return ContentUris.withAppendedId(contentUri, id)
}
}
return null
}

// ActivityAware implementation
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activityBinding = binding
binding.addActivityResultListener(this)
}

override fun onDetachedFromActivityForConfigChanges() {
activityBinding?.removeActivityResultListener(this)
activityBinding = null
}

override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activityBinding = binding
binding.addActivityResultListener(this)
}

override fun onDetachedFromActivity() {
activityBinding?.removeActivityResultListener(this)
activityBinding = null
}

// ActivityResultListener implementation
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == PERMISSION_REQUEST_CODE) {
val granted = hasManageStoragePermission()
pendingResult?.success(granted)
pendingResult = null
return true
}
return false
}
}

private const val TAG = "BackgroundServicePlugin"
private const val BUFFER_SIZE = 2 * 1024 * 1024
private const val BUFFER_SIZE = 2 * 1024 * 1024;
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package app.alextran.immich

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import androidx.annotation.NonNull
import android.os.Bundle
import android.content.Intent

class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
flutterEngine.plugins.add(BackgroundServicePlugin())
// No need to set up method channel here as it's now handled in the plugin
}

}
1 change: 0 additions & 1 deletion mobile/lib/domain/models/store.model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ enum StoreKey<T> {

// Video settings
loadOriginalVideo<bool>._(136),
manageLocalMediaAndroid<bool>._(137),

// Experimental stuff
photoManagerCustomFilter<bool>._(1000);
Expand Down
5 changes: 0 additions & 5 deletions mobile/lib/interfaces/local_files_manager.interface.dart

This file was deleted.

28 changes: 1 addition & 27 deletions mobile/lib/providers/websocket.provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ enum PendingAction {
assetDelete,
assetUploaded,
assetHidden,
assetTrash,
}

class PendingChange {
Expand Down Expand Up @@ -161,7 +160,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
socket.on('on_upload_success', _handleOnUploadSuccess);
socket.on('on_config_update', _handleOnConfigUpdate);
socket.on('on_asset_delete', _handleOnAssetDelete);
socket.on('on_asset_trash', _handleOnAssetTrash);
socket.on('on_asset_trash', _handleServerUpdates);
socket.on('on_asset_restore', _handleServerUpdates);
socket.on('on_asset_update', _handleServerUpdates);
socket.on('on_asset_stack_update', _handleServerUpdates);
Expand Down Expand Up @@ -208,26 +207,6 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
_debounce.run(handlePendingChanges);
}

Future<void> _handlePendingTrashes() async {
final trashChanges = state.pendingChanges
.where((c) => c.action == PendingAction.assetTrash)
.toList();
if (trashChanges.isNotEmpty) {
List<String> remoteIds = trashChanges
.expand((a) => (a.value as List).map((e) => e.toString()))
.toList();

await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
await _ref.read(assetProvider.notifier).getAllAsset();

state = state.copyWith(
pendingChanges: state.pendingChanges
.whereNot((c) => trashChanges.contains(c))
.toList(),
);
}
}

Future<void> _handlePendingDeletes() async {
final deleteChanges = state.pendingChanges
.where((c) => c.action == PendingAction.assetDelete)
Expand Down Expand Up @@ -288,7 +267,6 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
await _handlePendingUploaded();
await _handlePendingDeletes();
await _handlingPendingHidden();
await _handlePendingTrashes();
}

void _handleOnConfigUpdate(dynamic _) {
Expand All @@ -307,10 +285,6 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
void _handleOnAssetDelete(dynamic data) =>
addPendingChange(PendingAction.assetDelete, data);

void _handleOnAssetTrash(dynamic data) {
addPendingChange(PendingAction.assetTrash, data);
}

void _handleOnAssetHidden(dynamic data) =>
addPendingChange(PendingAction.assetHidden, data);

Expand Down
23 changes: 0 additions & 23 deletions mobile/lib/repositories/local_files_manager.repository.dart

This file was deleted.

1 change: 0 additions & 1 deletion mobile/lib/services/app_settings.service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ enum AppSettingsEnum<T> {
0,
),
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
Expand Down
Loading
Loading