use checksum for asset restoration

refactro code
This commit is contained in:
Peter Ombodi 2025-09-08 14:29:59 +03:00
parent b8274c9ed4
commit 40ac65db46
5 changed files with 82 additions and 40 deletions

View file

@ -24,6 +24,7 @@ import java.security.MessageDigest
import java.io.FileInputStream
import kotlinx.coroutines.*
import androidx.core.net.toUri
import java.io.InputStream
/**
* Android plugin for Dart `BackgroundService` and file trash operations
@ -155,14 +156,15 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
"restoreFromTrash" -> {
val fileName = call.argument<String>("fileName")
val type = call.argument<Int>("type")
if (fileName != null && type != null) {
val checksum = call.argument<String>("checksum")
if (fileName != null && type != null && checksum != null) {
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) {
restoreFromTrash(fileName, type, result)
restoreFromTrash(fileName, type, checksum, result)
} else {
result.error("PERMISSION_DENIED", "Media permission required", null)
}
} else {
result.error("INVALID_NAME", "The file name is not specified.", null)
result.error("INVALID_NAME", "The file name or checksum is not specified.", null)
}
}
@ -212,14 +214,14 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
}
@RequiresApi(Build.VERSION_CODES.R)
private fun restoreFromTrash(name: String, type: Int, result: Result) {
val uri = getTrashedFileUri(name, type)
private fun restoreFromTrash(name: String, type: Int, checksum: String, result: Result) {
val uri = getTrashedFileUri(name, type, checksum)
if (uri == null) {
Log.e("TrashError", "Asset Uri cannot be found obtained")
Log.e(TAG, "TrashError, Asset Uri cannot be found obtained")
result.error("TrashError", "Asset Uri cannot be found obtained", null)
return
}
Log.e("FILE_URI", uri.toString())
Log.i(TAG, "trying to restore from trash FILE_URI - $uri")
uri.let { toggleTrash(listOf(it), false, result) }
}
@ -231,7 +233,6 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
result.error("TrashError", "Activity or ContentResolver not available", null)
return
}
try {
val pendingIntent = MediaStore.createTrashRequest(contentResolver, contentUris, isTrashed)
pendingResult = result // Store for onActivityResult
@ -241,39 +242,68 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
null, 0, 0, 0
)
} catch (e: Exception) {
Log.e("TrashError", "Error creating or starting trash request", e)
Log.e(TAG, "TrashError, Error creating or starting trash request", e)
result.error("TrashError", "Error creating or starting trash request", null)
}
}
@RequiresApi(Build.VERSION_CODES.R)
private fun getTrashedFileUri(fileName: String, type: Int): Uri? {
private fun getTrashedFileUri(fileName: String, type: Int, checksum: String): Uri? {
val contentResolver = context?.contentResolver ?: return null
val queryUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
val projection = arrayOf(MediaStore.Files.FileColumns._ID)
val projection = arrayOf(
MediaStore.Files.FileColumns._ID,
MediaStore.Files.FileColumns.DISPLAY_NAME
)
val dotIndex = fileName.lastIndexOf('.')
val baseName = if (dotIndex != -1) fileName.substring(0, dotIndex) else fileName
val extension = if (dotIndex != -1) fileName.substring(dotIndex) else ""
val nameLike = "%${baseName}%${extension}"
val queryArgs = Bundle().apply {
putString(
ContentResolver.QUERY_ARG_SQL_SELECTION,
"${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?"
)
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(fileName))
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns.DISPLAY_NAME} LIKE ?")
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(nameLike))
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
}
val expectedBytes = try {
android.util.Base64.decode(checksum, android.util.Base64.DEFAULT)
} catch (e: Exception) {
Log.e(TAG, "getTrashedFileUri, invalid Base64 checksum: $checksum, Exception: $e")
return null
}
contentResolver.query(queryUri, projection, queryArgs, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
// same order as AssetType from dart
val contentUri = when (type) {
1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
else -> queryUri
val idxId = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
val idxName = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME)
while (cursor.moveToNext()) {
val id = cursor.getLong(idxId)
val dn = cursor.getString(idxName)
val candidateUri = ContentUris.withAppendedId(queryUri, id)
val sha1Bytes = try {
contentResolver.openInputStream(candidateUri)?.use { ins -> sha1OfStream(ins) }
} catch (e: Exception) {
Log.e(TAG, "getTrashedFileUri, hash failed for $dn: ${e.message}")
null
} ?: continue
if (sha1Bytes.contentEquals(expectedBytes)) {
// same order as AssetType from dart
val contentUri = when (type) {
1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
else -> queryUri
}
return ContentUris.withAppendedId(contentUri, id)
}
return ContentUris.withAppendedId(contentUri, id)
}
}
Log.w(TAG, "getTrashedFileUri, not found by checksum, nameLike=$nameLike, checksum: $checksum")
return null
}
@ -315,6 +345,18 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
}
return false
}
private fun sha1OfStream(ins: InputStream): ByteArray {
val buf = ByteArray(BUFFER_SIZE)
val md = MessageDigest.getInstance("SHA-1")
var len: Int
while (true) {
len = ins.read(buf)
if (len <= 0) break
md.update(buf, 0, len)
}
return md.digest()
}
}
private const val TAG = "BackgroundServicePlugin"

View file

@ -52,20 +52,16 @@ class TrashSyncService {
if (trashedAssetsChecksums.isEmpty) {
return Future.value();
} else {
final candidatesToTrash = await _localAssetRepository.getByChecksums(trashedAssetsChecksums);
if (candidatesToTrash.isNotEmpty) {
final groupedByChecksum = <String, LocalAsset>{};
for (final localAsset in candidatesToTrash) {
groupedByChecksum[localAsset.checksum!] = localAsset;
}
final localAssetsToTrash = await _localAssetRepository.getByChecksums(trashedAssetsChecksums);
if (localAssetsToTrash.isNotEmpty) {
final mediaUrls = await Future.wait(
groupedByChecksum.values.map(
localAssetsToTrash.map(
(localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl()),
),
);
_logger.info("Moving to trash ${mediaUrls.join(", ")} assets");
await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
await _localAssetRepository.delete(candidatesToTrash.map((asset) => asset.id));
await _localAssetRepository.delete(localAssetsToTrash.map((asset) => asset.id));
}
}
}
@ -79,9 +75,9 @@ class TrashSyncService {
isTrashed: true,
);
if (remoteAssetsToRestore.isNotEmpty) {
_logger.info("Restoring from trash ${remoteAssetsToRestore.map((e) => e.name).join(", ")} assets");
_logger.info("Restoring from trash ${remoteAssetsToRestore.map((e) => "${e.name}/${e.checksum}").join(", ")} assets");
for (RemoteAsset asset in remoteAssetsToRestore) {
await _localFilesManager.restoreFromTrash(asset.name, asset.type.index);
await _localFilesManager.restoreFromTrash(asset.name, asset.type.index, asset.checksum!);
}
}
}

View file

@ -14,8 +14,8 @@ class LocalFilesManagerRepository {
return await _service.moveToTrash(mediaUrls);
}
Future<bool> restoreFromTrash(String fileName, int type) async {
return await _service.restoreFromTrash(fileName, type);
Future<bool> restoreFromTrash(String fileName, int type, String checksum) async {
return await _service.restoreFromTrash(fileName, type, checksum);
}
Future<bool> requestManageMediaPermission() async {

View file

@ -18,9 +18,13 @@ class LocalFilesManagerService {
}
}
Future<bool> restoreFromTrash(String fileName, int type) async {
Future<bool> restoreFromTrash(String fileName, int type, String checksum) async {
try {
return await _channel.invokeMethod('restoreFromTrash', {'fileName': fileName, 'type': type});
return await _channel.invokeMethod('restoreFromTrash', {
'fileName': fileName,
'type': type,
'checksum': checksum,
});
} catch (e, s) {
_logger.warning('Error restore file from trash', e, s);
return false;

View file

@ -689,7 +689,7 @@ class SyncService {
}
trashMediaUrls.add(mediaUrl);
} else {
await _localFilesManager.restoreFromTrash(asset.fileName, asset.type.index);
await _localFilesManager.restoreFromTrash(asset.fileName, asset.type.index, asset.checksum);
}
}