Skip to content

Commit 7b8df5e

Browse files
ColaFantaAlexV525
andauthored
feat: Implement isFavorite for Android R+ (#1312)
## Description: This PR implements the AssetEntity.isFavorite property for Android 11 (R) and above. ## Changes: * Introduces a `PhotoManagerFavoriteManager` class to handle the new MediaStore.MediaColumns.IS_FAVORITE column. * Populates the `isFavorite` property by querying the IS_FAVORITE value from the MediaStore. For Android 10 and below, it is always false as before. * Updates the example project with a functional "Toggle isFavorite" button to demonstrate the feature. CLOSES #1311 --------- Co-authored-by: Alex Li <[email protected]>
1 parent 47eca57 commit 7b8df5e

File tree

12 files changed

+140
-33
lines changed

12 files changed

+140
-33
lines changed

android/src/main/kotlin/com/fluttercandies/photo_manager/PhotoManagerPlugin.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class PhotoManagerPlugin : FlutterPlugin, ActivityAware {
8484
binding.addRequestPermissionsResultListener(listener)
8585
plugin?.let {
8686
binding.addActivityResultListener(it.deleteManager)
87+
binding.addActivityResultListener(it.favoriteManager)
8788
}
8889
}
8990

@@ -93,6 +94,7 @@ class PhotoManagerPlugin : FlutterPlugin, ActivityAware {
9394
}
9495
plugin?.let { p ->
9596
oldBinding.removeActivityResultListener(p.deleteManager)
97+
oldBinding.removeActivityResultListener(p.favoriteManager)
9698
}
9799
}
98100
}

android/src/main/kotlin/com/fluttercandies/photo_manager/constant/Methods.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class Methods {
8181
const val saveImage = "saveImage"
8282
const val saveImageWithPath = "saveImageWithPath"
8383
const val saveVideo = "saveVideo"
84+
const val favoriteAsset = "favoriteAsset"
8485
const val copyAsset = "copyAsset"
8586
const val moveAssetToPath = "moveAssetToPath"
8687
const val removeNoExistsAssets = "removeNoExistsAssets"
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.fluttercandies.photo_manager.core
2+
3+
import android.app.Activity
4+
import android.content.ContentResolver
5+
import android.content.Context
6+
import android.content.Intent
7+
import android.net.Uri
8+
import android.os.Build
9+
import android.provider.MediaStore
10+
import androidx.annotation.RequiresApi
11+
import com.fluttercandies.photo_manager.util.ResultHandler
12+
import io.flutter.plugin.common.PluginRegistry
13+
14+
class PhotoManagerFavoriteManager(val context: Context) :
15+
PluginRegistry.ActivityResultListener {
16+
17+
var activity: Activity? = null
18+
19+
fun bindActivity(activity: Activity?) {
20+
this.activity = activity
21+
}
22+
23+
private val requestCode = 40071
24+
25+
private var resultHandler: ResultHandler? = null
26+
27+
private val cr: ContentResolver
28+
get() = context.contentResolver
29+
30+
@RequiresApi(Build.VERSION_CODES.R)
31+
fun favoriteAsset(assetUri: Uri, isFavorite: Boolean, resultHandler: ResultHandler) {
32+
this.resultHandler = resultHandler
33+
34+
val pi = MediaStore.createFavoriteRequest(
35+
context.contentResolver,
36+
setOf(assetUri), isFavorite
37+
)
38+
activity?.startIntentSenderForResult(
39+
pi.intentSender,
40+
requestCode,
41+
null,
42+
0,
43+
0,
44+
0
45+
)
46+
}
47+
48+
49+
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?): Boolean {
50+
if (requestCode != this.requestCode)
51+
return false
52+
53+
resultHandler?.reply(resultCode == Activity.RESULT_OK)
54+
resultHandler = null
55+
return true
56+
57+
}
58+
59+
}

android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManagerPlugin.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,13 @@ class PhotoManagerPlugin(
5858
}
5959

6060
val deleteManager = PhotoManagerDeleteManager(applicationContext, activity)
61+
val favoriteManager = PhotoManagerFavoriteManager(applicationContext)
6162

6263
fun bindActivity(activity: Activity?) {
6364
this.activity = activity
6465
permissionsUtils.withActivity(activity)
6566
deleteManager.bindActivity(activity)
67+
favoriteManager.bindActivity(activity)
6668
}
6769

6870
private val notifyChannel = PhotoManagerNotifyChannel(
@@ -541,6 +543,17 @@ class PhotoManagerPlugin(
541543
}
542544
}
543545

546+
Methods.favoriteAsset -> {
547+
val assetId = call.argument<String>("id")!!
548+
val isFavorite = call.argument<Boolean>("favorite")!!
549+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
550+
LogUtils.error("The API 30 or lower have no IS_FAVORITE row in MediaStore.")
551+
resultHandler.reply(false)
552+
return
553+
}
554+
favoriteManager.favoriteAsset(photoManager.getUri(assetId), isFavorite, resultHandler)
555+
}
556+
544557
Methods.copyAsset -> {
545558
val assetId = call.argument<String>("assetId")!!
546559
val galleryId = call.argument<String>("galleryId")!!

android/src/main/kotlin/com/fluttercandies/photo_manager/core/entity/AssetEntity.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ data class AssetEntity(
1616
val displayName: String,
1717
val modifiedDate: Long,
1818
val orientation: Int,
19+
val isFavorite: Boolean = false,
1920
val lat: Double? = null,
2021
val lng: Double? = null,
2122
val androidQRelativePath: String? = null,

android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/ConvertUtils.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ object ConvertUtils {
5050
"width" to entity.width,
5151
"height" to entity.height,
5252
"orientation" to entity.orientation,
53+
"is_favorite" to entity.isFavorite,
5354
"modifiedDt" to entity.modifiedDate,
5455
"lat" to entity.lat,
5556
"lng" to entity.lng,

android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/IDBUtils.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import android.provider.MediaStore.MediaColumns.DATE_TAKEN
1919
import android.provider.MediaStore.MediaColumns.DISPLAY_NAME
2020
import android.provider.MediaStore.MediaColumns.DURATION
2121
import android.provider.MediaStore.MediaColumns.HEIGHT
22+
import android.provider.MediaStore.MediaColumns.IS_FAVORITE
2223
import android.provider.MediaStore.MediaColumns.MIME_TYPE
2324
import android.provider.MediaStore.MediaColumns.ORIENTATION
2425
import android.provider.MediaStore.MediaColumns.RELATIVE_PATH
@@ -27,6 +28,7 @@ import android.provider.MediaStore.MediaColumns.WIDTH
2728
import android.provider.MediaStore.MediaColumns._ID
2829
import android.provider.MediaStore.VOLUME_EXTERNAL
2930
import androidx.annotation.ChecksSdkIntAtLeast
31+
import androidx.annotation.RequiresApi
3032
import androidx.exifinterface.media.ExifInterface
3133
import com.fluttercandies.photo_manager.core.PhotoManager
3234
import com.fluttercandies.photo_manager.core.entity.AssetEntity
@@ -61,6 +63,7 @@ interface IDBUtils {
6163
DATE_TAKEN //日期
6264
).apply {
6365
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) add(DATE_TAKEN) // 拍摄时间
66+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) add(IS_FAVORITE)
6467
}
6568

6669
val storeVideoKeys = mutableListOf(
@@ -79,6 +82,7 @@ interface IDBUtils {
7982
DURATION //时长
8083
).apply {
8184
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) add(DATE_TAKEN) // 拍摄时间
85+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) add(IS_FAVORITE)
8286
}
8387

8488
val typeKeys = arrayOf(
@@ -199,6 +203,7 @@ interface IDBUtils {
199203
val displayName = getString(DISPLAY_NAME)
200204
val modifiedDate = getLong(DATE_MODIFIED)
201205
var orientation: Int = getInt(ORIENTATION)
206+
val isFavorite = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && getInt(IS_FAVORITE) == 1
202207
val relativePath: String? = if (isAboveAndroidQ) {
203208
getString(RELATIVE_PATH)
204209
} else null
@@ -240,6 +245,7 @@ interface IDBUtils {
240245
displayName,
241246
modifiedDate,
242247
orientation,
248+
isFavorite,
243249
androidQRelativePath = relativePath,
244250
mimeType = mimeType
245251
)

example/lib/page/image_list_page.dart

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import '../util/log.dart';
1515
import '../widget/dialog/list_dialog.dart';
1616
import '../widget/image_item_widget.dart';
1717
import '../widget/loading_widget.dart';
18-
1918
import 'copy_to_another_gallery_example.dart';
2019
import 'detail_page.dart';
2120
import 'move_to_another_gallery_example.dart';
@@ -207,36 +206,40 @@ class _GalleryContentListPageState extends State<GalleryContentListPage> {
207206
<int>[500, 600, 700, 1000, 1500, 2000],
208207
),
209208
),
210-
if (Platform.isIOS || Platform.isMacOS || PlatformUtils.isOhos)
211-
ElevatedButton(
212-
child: const Text('Toggle isFavorite'),
213-
onPressed: () async {
214-
final bool isFavorite = entity.isFavorite;
215-
print('Current isFavorite: $isFavorite');
216-
if (PlatformUtils.isOhos) {
217-
await PhotoManager.editor.ohos.favoriteAsset(
218-
entity: entity,
219-
favorite: !isFavorite,
220-
);
221-
} else {
222-
await PhotoManager.editor.darwin.favoriteAsset(
223-
entity: entity,
224-
favorite: !isFavorite,
225-
);
226-
}
227-
final AssetEntity? newEntity =
228-
await entity.obtainForNewProperties();
229-
print('New isFavorite: ${newEntity?.isFavorite}');
230-
if (!mounted) {
231-
return;
232-
}
233-
if (newEntity != null) {
234-
entity = newEntity;
235-
readPathProvider(context).list[index] = newEntity;
236-
setState(() {});
237-
}
238-
},
239-
),
209+
ElevatedButton(
210+
child: const Text('Toggle isFavorite'),
211+
onPressed: () async {
212+
final bool isFavorite = entity.isFavorite;
213+
print('Current isFavorite: $isFavorite');
214+
if (PlatformUtils.isOhos) {
215+
await PhotoManager.editor.ohos.favoriteAsset(
216+
entity: entity,
217+
favorite: !isFavorite,
218+
);
219+
} else if (Platform.isAndroid) {
220+
await PhotoManager.editor.android.favoriteAsset(
221+
entity: entity,
222+
favorite: !isFavorite,
223+
);
224+
} else {
225+
await PhotoManager.editor.darwin.favoriteAsset(
226+
entity: entity,
227+
favorite: !isFavorite,
228+
);
229+
}
230+
final AssetEntity? newEntity =
231+
await entity.obtainForNewProperties();
232+
print('New isFavorite: ${newEntity?.isFavorite}');
233+
if (!mounted) {
234+
return;
235+
}
236+
if (newEntity != null) {
237+
entity = newEntity;
238+
readPathProvider(context).list[index] = newEntity;
239+
setState(() {});
240+
}
241+
},
242+
),
240243
if ((Platform.isIOS || Platform.isMacOS) && entity.isLivePhoto)
241244
ElevatedButton(
242245
onPressed: () {

example/lib/util/common_util.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class CommonUtil {
4949
_buildInfoItemAsync('title', entity.titleAsync),
5050
_buildInfoItem('lat', lat.toString()),
5151
_buildInfoItem('lng', lng.toString()),
52+
_buildInfoItem('is favorite', entity.isFavorite.toString()),
5253
_buildInfoItem('relative path', entity.relativePath ?? 'null'),
5354
_buildInfoItemAsync('mimeType', entity.mimeTypeAsync),
5455
],

lib/src/internal/editor.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,23 @@ class AndroidEditor {
305305
/// Creates a new [AndroidEditor] object.
306306
const AndroidEditor();
307307

308+
/// Sets the favorite status of the given [entity].
309+
///
310+
/// Returns the updated [AssetEntity] if the operation was successful; otherwise, throws a state error to indicate the failure.
311+
Future<AssetEntity> favoriteAsset({
312+
required AssetEntity entity,
313+
required bool favorite,
314+
}) async {
315+
final bool result = await plugin.favoriteAsset(entity.id, favorite);
316+
if (result) {
317+
return entity.copyWith(isFavorite: favorite);
318+
}
319+
throw StateError(
320+
'Failed to favorite the asset '
321+
'${entity.id} for unknown reason',
322+
);
323+
}
324+
308325
/// Moves the given [entity] to the specified [target] path.
309326
///
310327
/// Returns `true` if the move was successful; otherwise, `false`.

0 commit comments

Comments
 (0)