Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ To know more about breaking changes, see the [Migration Guide][].
- Fix Android save/copy operations leaving visible incomplete MediaStore items when the write fails (#1434).
- Fix Android thumbnail requests not deterministically releasing Glide resources (#1436).
- Fix Android path-based saves leaking file descriptors by not closing inspection streams (#1438).
- Fix Android change notifications reporting inserts of not-yet-published rows (e.g. files pushed via `adb push` or desktop drag-and-drop) as deletions (#1443).

## 3.12.0

Expand Down
8 changes: 8 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ analyzer:
prefer_single_quotes: warning
require_trailing_commas: warning
invalid_annotation_target: false
exclude:
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**

linter:
rules:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,54 +127,97 @@ class PhotoManagerNotifyChannel(
}

override fun onChange(selfChange: Boolean, uri: Uri?) {
// Dispatch target on frameworks below API 30, which have no
// change-kind flags; the type is inferred from the row state.
handleOnChange(uri, null)
}

override fun onChange(selfChange: Boolean, uri: Uri?, flags: Int) {
// Dispatch target on API 30+, where MediaProvider tags row
// notifications with the kind of change. Untagged notifications
// (flags without any kind bit, e.g. plain
// ContentResolver#notifyChange calls) fall back to the row-state
// inference used pre-30.
val kind = flags and (
ContentResolver.NOTIFY_INSERT
or ContentResolver.NOTIFY_UPDATE
or ContentResolver.NOTIFY_DELETE
)
handleOnChange(
uri,
when (kind) {
ContentResolver.NOTIFY_INSERT -> "insert"
ContentResolver.NOTIFY_UPDATE -> "update"
ContentResolver.NOTIFY_DELETE -> "delete"
else -> null
},
)
}

private fun handleOnChange(uri: Uri?, authoritativeType: String?) {
if (uri == null) {
return
}
val last = uri.lastPathSegment
val id = last?.toLongOrNull()

if (id != null) { // insert or update
val cursor = safeQuery(
allUri,
arrayOf(DATE_ADDED, DATE_MODIFIED, MEDIA_TYPE),
"$_ID = ?",
arrayOf(id.toString())
)
cursor?.use {
if (!cursor.moveToNext()) {
// If the ID not have item, make it as deleted.
onOuterChange(uri, "delete", id, null, type)
if (id == null) { // collection-level change
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (uri == this.uri) {
onOuterChange(uri, "insert", null, null, type)
return
}
// Find date to determine insert or update.
val addTimestampSecond = cursor.getLong(cursor.getColumnIndex(DATE_ADDED))
val currentTimeMillis = System.currentTimeMillis()
}
onOuterChange(uri, "delete", null, null, type)
return
}

if (authoritativeType == "delete") {
// The row is gone; no point in querying it.
onOuterChange(uri, "delete", id, null, type)
return
}

val diffTime = currentTimeMillis / 1000 - addTimestampSecond
val cursor = safeQuery(
allUri,
arrayOf(DATE_ADDED, DATE_MODIFIED, MEDIA_TYPE),
"$_ID = ?",
arrayOf(id.toString())
)
cursor?.use {
if (!it.moveToNext()) {
// The row is not visible to this app. On Android 10+,
// rows inserted by other packages — such as files pushed
// via `adb push` or desktop drag-and-drop to an emulator
// — stay pending and hidden until their owner publishes
// them (#1443). An INSERT flag still proves an insert,
// but every other invisible outcome (a trashed row, a
// still-pending row being updated, a real deletion with
// an untagged notification) is reported as "delete",
// matching the historical behavior for missing rows.
val typeString =
if (authoritativeType == "insert") "insert" else "delete"
onOuterChange(uri, typeString, id, null, type)
return
}
// Find date to determine insert or update for untagged events.
val typeString = authoritativeType ?: run {
val addTimestampSecond = it.getLong(it.getColumnIndex(DATE_ADDED))

// Within 30s, it is considered to be inserted, if it is exceeded, it is considered to be changed
val typeString = if (diffTime < 30) {
if (System.currentTimeMillis() / 1000 - addTimestampSecond < 30) {
"insert"
} else {
"update"
}
// get Type
val type = cursor.getInt(cursor.getColumnIndex(MEDIA_TYPE))
val (gId, gName) = getGalleryIdAndName(id, type)

if (gId == null || gName == null) {
return
}
onOuterChange(uri, typeString, id, gId, type)
}
} else { // delete
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (uri == this.uri) {
onOuterChange(uri, "insert", null, null, type)
return
}
}
onOuterChange(uri, "delete", null, null, type)
// get Type
val mediaType = it.getInt(it.getColumnIndex(MEDIA_TYPE))
val (gId, _) = getGalleryIdAndName(id, mediaType)

// The gallery may fail to resolve in racy cases; galleryId
// is optional in the payload, so still deliver the event.
onOuterChange(uri, typeString, id, gId, mediaType)
}
}

Expand Down
10 changes: 10 additions & 0 deletions example/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
include: ../analysis_options.yaml

analyzer:
exclude:
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**

linter:
rules:
avoid_print: false
Expand Down
2 changes: 1 addition & 1 deletion example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ android {

defaultConfig {
applicationId "com.fluttercandies.photo_manager_example"
minSdkVersion 21
minSdkVersion flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
Expand Down
4 changes: 4 additions & 0 deletions example/android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@ org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryErro
android.useAndroidX=true
android.enableJetifier=true
kotlin.code.style=official
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
2 changes: 1 addition & 1 deletion example/android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
8 changes: 4 additions & 4 deletions example/android/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ pluginManagement {

plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.9.3" apply false
id "com.android.library" version "8.9.3" apply false
id "org.jetbrains.kotlin.android" version "2.1.20" apply false
id "org.jetbrains.kotlin.kapt" version "2.1.20" apply false
id "com.android.application" version "8.11.1" apply false
id "com.android.library" version "8.11.1" apply false
id "org.jetbrains.kotlin.android" version "2.2.20" apply false
id "org.jetbrains.kotlin.kapt" version "2.2.20" apply false
}

include ":app"
Loading