Skip to content
Draft
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
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@ To know more about breaking changes, see the [Migration Guide][].

## Unreleased

*None.*
**Features**

- Add native photo picker support for iOS/Android without requiring storage permissions:
- New `PhotoManager.pickAssets()` method with `maxCount`, `requestType`, and `useItemProvider` parameters.
- **iOS 14+**: Uses `PHPickerViewController` without requiring photo library permissions.
- **Android 11+ (API 30+)**: Uses native Photo Picker API (ACTION_PICK_IMAGES).
- **Android < 11**: Falls back to legacy `ACTION_PICK` intent, also works without permissions.
- **macOS**: Limited support (PHPickerViewController not fully available).
- Returns standard `AssetEntity` objects compatible with all photo_manager APIs.
- Works independently of app's permission status.

## 3.8.3

Expand Down
43 changes: 43 additions & 0 deletions README-ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,49 @@ Android 10 引入了 **Scoped Storage**,导致原始资源文件不能通过

## 使用方法

### 原生照片选择器(无需权限)

从 3.9.0 版本开始,`photo_manager` 提供了原生照片选择器,无需存储权限。这非常适合只需要让用户选择照片/视频而不需要完整相册访问权限的应用。

```dart
// 选择最多 9 个资源(图片和视频)
final List<AssetEntity> assets = await PhotoManager.pickAssets(
maxCount: 9,
requestType: RequestType.common, // 或者 RequestType.image, RequestType.video
);

if (assets.isNotEmpty) {
// 使用选中的资源
for (final asset in assets) {
final file = await asset.file;
// 处理文件
}
}
```

**平台支持:**
- **Android 11+ (API 30+)**:使用原生照片选择器 API
- **Android < 11**:使用传统的 `ACTION_PICK` intent(同样无需权限)
- **iOS 14+**:使用 `PHPickerViewController`
- **macOS**:有限支持(可能在某些版本上不可用)

**主要优势:**
- ✅ 无需存储权限
- ✅ 原生系统选择器界面
- ✅ 返回标准的 `AssetEntity` 对象
- ✅ 与其他 photo_manager API 完全兼容
- ✅ 即使权限被拒绝也能工作

**参数说明:**
- `maxCount`:可选择的最大资源数量(默认:9)。注意:在 Android < 11 上仅支持单选。
- `requestType`:允许的媒体类型:
- `RequestType.common`:图片和视频(默认)
- `RequestType.image`:仅图片
- `RequestType.video`:仅视频
- `useItemProvider`:(仅 iOS)处理 iCloud 资源。默认:false。**注意**:完整的 iCloud 支持计划在未来版本中实现。目前仅返回本地库中可用的资源。

> **注意**:此方法不需要先调用 `requestPermissionExtend()`。它独立于应用的权限状态工作。

### 请求权限

大部分的 API 只在获取到权限后才能正常使用。
Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,49 @@ It's pretty much the same as the `NSPhotoLibraryUsageDescription`.

## Usage

### Native photo picker (no permissions required)

Starting from version 3.9.0, `photo_manager` provides a native photo picker that doesn't require storage permissions. This is perfect for apps that only need to let users select photos/videos without needing full library access.

```dart
// Pick up to 9 assets (images and videos)
final List<AssetEntity> assets = await PhotoManager.pickAssets(
maxCount: 9,
requestType: RequestType.common, // or RequestType.image, RequestType.video
);

if (assets.isNotEmpty) {
// Use the selected assets
for (final asset in assets) {
final file = await asset.file;
// Do something with the file
}
}
```

**Platform Support:**
- **Android 11+ (API 30+)**: Uses the native Photo Picker API
- **Android < 11**: Uses legacy `ACTION_PICK` intent (also works without permissions)
- **iOS 14+**: Uses `PHPickerViewController`
- **macOS**: Limited support (may not work on all versions)

**Key Benefits:**
- ✅ No storage permissions required
- ✅ Native OS picker UI
- ✅ Returns standard `AssetEntity` objects
- ✅ Full compatibility with other photo_manager APIs
- ✅ Works even when permission is denied

**Parameters:**
- `maxCount`: Maximum number of assets to select (default: 9). Note: On Android < 11, only single selection is supported.
- `requestType`: Type of media to allow:
- `RequestType.common`: Both images and videos (default)
- `RequestType.image`: Only images
- `RequestType.video`: Only videos
- `useItemProvider`: (iOS only) Handle iCloud assets. Default: false. **Note**: Full iCloud support is planned for a future release. Currently, only assets available in the local library will be returned.

> **Note**: This method does not require calling `requestPermissionExtend()` first. It works independently of the app's permission status.

### Request for permission

Most of the APIs can only use with granted permission.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ class PhotoManagerPlugin : FlutterPlugin, ActivityAware {
binding.addActivityResultListener(it.deleteManager)
binding.addActivityResultListener(it.writeManager)
binding.addActivityResultListener(it.favoriteManager)
binding.addActivityResultListener(it.pickerManager)
}
}

Expand All @@ -97,6 +98,7 @@ class PhotoManagerPlugin : FlutterPlugin, ActivityAware {
oldBinding.removeActivityResultListener(p.deleteManager)
oldBinding.removeActivityResultListener(p.writeManager)
oldBinding.removeActivityResultListener(p.favoriteManager)
oldBinding.removeActivityResultListener(p.pickerManager)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Methods {
const val releaseMemoryCache = "releaseMemoryCache"
const val ignorePermissionCheck = "ignorePermissionCheck"
const val getPermissionState = "getPermissionState"
const val picker = "picker"

fun isNotNeedPermissionMethod(method: String): Boolean {
return method in arrayOf(
Expand All @@ -23,6 +24,7 @@ class Methods {
releaseMemoryCache,
ignorePermissionCheck,
getPermissionState,
picker,
)
}
// Not need permission methods end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,4 +368,12 @@ class PhotoManager(private val context: Context) {
val list = dbUtils.getAssetsByRange(context, option, start, end, requestType)
resultHandler.reply(ConvertUtils.convertAssets(list))
}

/**
* Convert URIs from the photo picker to AssetEntity objects.
* This method queries the MediaStore to get asset information from the URIs.
*/
fun getAssetsFromUris(uris: List<Uri>): List<AssetEntity> {
return dbUtils.getAssetsFromUris(context, uris)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package com.fluttercandies.photo_manager.core

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import com.fluttercandies.photo_manager.core.utils.ConvertUtils
import com.fluttercandies.photo_manager.util.ResultHandler
import io.flutter.plugin.common.PluginRegistry

/**
* Manager for handling native photo picker operations.
*
* On Android 11+ (API 30+), uses the modern Photo Picker API (ACTION_PICK_IMAGES).
* On older Android versions, falls back to the legacy ACTION_PICK intent.
* Both approaches work without requiring storage permissions.
*/
class PhotoManagerPickerManager(val context: Context) :
PluginRegistry.ActivityResultListener {

var activity: Activity? = null
private val photoManager = PhotoManager(context)

fun bindActivity(activity: Activity?) {
this.activity = activity
}

// Request codes for different picker modes
private val requestCodeModern = 40072 // For Photo Picker API (Android 11+)
private val requestCodeLegacy = 40073 // For ACTION_PICK (older Android)

private var resultHandler: ResultHandler? = null

/**
* Launch the appropriate picker based on Android version.
*/
fun launchPicker(activity: Activity, maxCount: Int, type: Int, resultHandler: ResultHandler) {
this.resultHandler = resultHandler

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11+ (API 30+): Use modern Photo Picker API
launchModernPicker(activity, maxCount, type)
} else {
// Older Android: Use legacy ACTION_PICK intent
launchLegacyPicker(activity, type)
}
}

/**
* Launch the modern Photo Picker API (Android 11+).
* Uses ACTION_PICK_IMAGES which doesn't require storage permissions.
*/
@RequiresApi(Build.VERSION_CODES.R)
private fun launchModernPicker(activity: Activity, maxCount: Int, type: Int) {
try {
val intent = Intent(MediaStore.ACTION_PICK_IMAGES)

// Set maximum number of selectable items
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxCount)

// Set media type filter
when (type) {
1 -> {
// Images only
intent.type = "image/*"
}
2 -> {
// Videos only
intent.type = "video/*"
}
else -> {
// Common (both images and videos) - don't set type to allow both
}
}

activity.startActivityForResult(intent, requestCodeModern)
} catch (e: Exception) {
resultHandler?.replyError("Failed to launch photo picker: ${e.message}")
this.resultHandler = null
}
}

/**
* Launch the legacy ACTION_PICK intent (Android < 11).
* This also works without storage permissions on most devices.
*/
private fun launchLegacyPicker(activity: Activity, type: Int) {
try {
val intent: Intent

when (type) {
1 -> {
// Images only
intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
}
2 -> {
// Videos only
intent = Intent(Intent.ACTION_PICK, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
}
else -> {
// Common (both images and videos)
// Use generic ACTION_PICK with Images URI as base
// Note: Legacy picker only supports single selection
intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
}
}

activity.startActivityForResult(intent, requestCodeLegacy)
} catch (e: Exception) {
resultHandler?.replyError("Failed to launch picker: ${e.message}")
this.resultHandler = null
}
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
// Only handle our request codes
if (requestCode != requestCodeModern && requestCode != requestCodeLegacy) {
return false
}

val handler = resultHandler ?: return false
resultHandler = null

if (resultCode != Activity.RESULT_OK) {
// User cancelled
handler.reply(mapOf("data" to emptyList<Any>()))
return true
}

// Process selected URIs
val uris = mutableListOf<Uri>()

if (requestCode == requestCodeModern && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Modern Photo Picker API supports multiple selection
data?.clipData?.let { clipData ->
for (i in 0 until clipData.itemCount) {
clipData.getItemAt(i)?.uri?.let { uri ->
uris.add(uri)
}
}
} ?: data?.data?.let { uri ->
// Single item selected
uris.add(uri)
}
} else {
// Legacy picker only supports single selection
data?.data?.let { uri ->
uris.add(uri)
}
}

if (uris.isEmpty()) {
handler.reply(mapOf("data" to emptyList<Any>()))
return true
}

// Convert URIs to AssetEntity objects in background thread
PhotoManagerPlugin.runOnBackground {
val assets = photoManager.getAssetsFromUris(uris)
val result = ConvertUtils.convertAssets(assets)

// Reply on main thread
Handler(Looper.getMainLooper()).post {
handler.reply(mapOf("data" to result))
}
}

return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,15 @@ class PhotoManagerPlugin(
val deleteManager = PhotoManagerDeleteManager(applicationContext, activity)
val writeManager = PhotoManagerWriteManager(applicationContext, activity)
val favoriteManager = PhotoManagerFavoriteManager(applicationContext)
val pickerManager = PhotoManagerPickerManager(applicationContext)

fun bindActivity(activity: Activity?) {
this.activity = activity
permissionsUtils.withActivity(activity)
deleteManager.bindActivity(activity)
writeManager.bindActivity(activity)
favoriteManager.bindActivity(activity)
pickerManager.bindActivity(activity)
}

private val notifyChannel = PhotoManagerNotifyChannel(
Expand Down Expand Up @@ -336,9 +338,27 @@ class PhotoManagerPlugin(
resultHandler.reply(it.value)
}
}

Methods.picker -> {
handlePickerMethod(resultHandler)
}
}
}

private fun handlePickerMethod(resultHandler: ResultHandler) {
if (activity == null) {
resultHandler.replyError("Activity is null. Cannot launch picker.")
return
}

val call = resultHandler.call
val maxCount = call.argument<Int>("maxCount") ?: 9
val type = call.argument<Int>("type") ?: 0

// Launch the picker - it will handle both modern and legacy approaches
pickerManager.launchPicker(activity!!, maxCount, type, resultHandler)
}

private fun handleMethodResult(
resultHandler: ResultHandler,
needLocationPermission: Boolean
Expand Down
Loading
Loading