@granite-js/image 1.0.11 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/android/src/coil/java/run/granite/image/providers/CoilImageProvider.kt +82 -69
- package/android/src/glide/java/run/granite/image/providers/GlideImageProvider.kt +109 -86
- package/android/src/main/java/run/granite/image/GraniteImage.kt +61 -73
- package/android/src/main/java/run/granite/image/GraniteImageManager.kt +5 -7
- package/android/src/main/java/run/granite/image/GraniteImageModule.kt +118 -74
- package/android/src/main/java/run/granite/image/GraniteImagePackage.kt +11 -13
- package/android/src/main/java/run/granite/image/GraniteImageProvider.kt +33 -2
- package/android/src/okhttp/java/run/granite/image/providers/OkHttpImageProvider.kt +113 -92
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @granite-js/image
|
|
2
2
|
|
|
3
|
+
## 1.0.13
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e26b4ea: refactor(android): improve GraniteImage code quality - fix bugs, reduce complexity, introduce DI
|
|
8
|
+
- eddab4b: feat(android): implement clearMemoryCache/clearDiskCache for GraniteImage Android
|
|
9
|
+
|
|
10
|
+
## 1.0.12
|
|
11
|
+
|
|
3
12
|
## 1.0.11
|
|
4
13
|
|
|
5
14
|
### Patch Changes
|
|
@@ -4,6 +4,8 @@ import android.content.Context
|
|
|
4
4
|
import android.graphics.Bitmap
|
|
5
5
|
import android.graphics.PorterDuff
|
|
6
6
|
import android.graphics.PorterDuffColorFilter
|
|
7
|
+
import android.graphics.drawable.BitmapDrawable
|
|
8
|
+
import android.net.Uri
|
|
7
9
|
import android.util.Log
|
|
8
10
|
import android.view.View
|
|
9
11
|
import android.widget.ImageView
|
|
@@ -12,34 +14,26 @@ import run.granite.image.GraniteImagePriority
|
|
|
12
14
|
import run.granite.image.GraniteImageCachePolicy
|
|
13
15
|
import run.granite.image.GraniteImageProgressCallback
|
|
14
16
|
import run.granite.image.GraniteImageCompletionCallback
|
|
17
|
+
import coil.Coil
|
|
15
18
|
import coil.load
|
|
16
19
|
import coil.request.CachePolicy
|
|
17
20
|
import coil.request.ErrorResult
|
|
21
|
+
import coil.request.ImageRequest
|
|
18
22
|
import coil.request.SuccessResult
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
25
|
* GraniteImageProvider implementation using Coil.
|
|
22
26
|
*/
|
|
23
27
|
class CoilImageProvider : GraniteImageProvider {
|
|
24
|
-
companion object {
|
|
25
|
-
private const val TAG = "CoilImageProvider"
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
override fun createImageView(context: Context): View {
|
|
29
|
-
return ImageView(context).apply {
|
|
30
|
-
setBackgroundColor(android.graphics.Color.LTGRAY)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
28
|
override fun loadImage(url: String, into: View, scaleType: ImageView.ScaleType) {
|
|
35
29
|
loadImage(url, into, scaleType, null, GraniteImagePriority.NORMAL, GraniteImageCachePolicy.DISK, null, null, null)
|
|
36
30
|
}
|
|
37
31
|
|
|
38
32
|
private fun isValidImageUrl(url: String): Boolean {
|
|
39
33
|
return try {
|
|
40
|
-
val uri =
|
|
34
|
+
val uri = Uri.parse(url)
|
|
41
35
|
val scheme = uri.scheme?.lowercase()
|
|
42
|
-
scheme == "http" || scheme == "https"
|
|
36
|
+
scheme == "http" || scheme == "https" || scheme == "file" || scheme == "content"
|
|
43
37
|
} catch (e: Exception) {
|
|
44
38
|
false
|
|
45
39
|
}
|
|
@@ -56,22 +50,7 @@ class CoilImageProvider : GraniteImageProvider {
|
|
|
56
50
|
progressCallback: GraniteImageProgressCallback?,
|
|
57
51
|
completionCallback: GraniteImageCompletionCallback?
|
|
58
52
|
) {
|
|
59
|
-
val imageView
|
|
60
|
-
if (into !is ImageView) {
|
|
61
|
-
Log.e(TAG, "View is not an ImageView")
|
|
62
|
-
completionCallback?.invoke(null, Exception("View is not an ImageView"), 0, 0)
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
into.scaleType = scaleType
|
|
66
|
-
into
|
|
67
|
-
} else {
|
|
68
|
-
null
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (imageView == null) {
|
|
72
|
-
completionCallback?.invoke(null, Exception("No view provided"), 0, 0)
|
|
73
|
-
return
|
|
74
|
-
}
|
|
53
|
+
val imageView = validateImageView(into, scaleType, completionCallback) ?: return
|
|
75
54
|
|
|
76
55
|
if (!isValidImageUrl(url)) {
|
|
77
56
|
Log.e(TAG, "Invalid URL: $url")
|
|
@@ -80,54 +59,74 @@ class CoilImageProvider : GraniteImageProvider {
|
|
|
80
59
|
}
|
|
81
60
|
|
|
82
61
|
imageView.load(url) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
62
|
+
headers?.forEach { (key, value) -> addHeader(key, value) }
|
|
63
|
+
applyCachePolicy(cachePolicy)
|
|
64
|
+
applyPlaceholder(imageView.context, defaultSource)
|
|
65
|
+
listener(
|
|
66
|
+
onStart = { Log.d(TAG, "Loading started: $url") },
|
|
67
|
+
onSuccess = { _, result -> handleSuccess(result, url, completionCallback) },
|
|
68
|
+
onError = { _, result -> handleError(result, completionCallback) }
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
87
72
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
73
|
+
private fun validateImageView(
|
|
74
|
+
into: View?,
|
|
75
|
+
scaleType: ImageView.ScaleType,
|
|
76
|
+
completionCallback: GraniteImageCompletionCallback?
|
|
77
|
+
): ImageView? {
|
|
78
|
+
if (into == null) {
|
|
79
|
+
completionCallback?.invoke(null, Exception("No view provided"), 0, 0)
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
if (into !is ImageView) {
|
|
83
|
+
Log.e(TAG, "View is not an ImageView")
|
|
84
|
+
completionCallback?.invoke(null, Exception("View is not an ImageView"), 0, 0)
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
into.scaleType = scaleType
|
|
88
|
+
return into
|
|
89
|
+
}
|
|
103
90
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
91
|
+
private fun ImageRequest.Builder.applyCachePolicy(cachePolicy: GraniteImageCachePolicy) {
|
|
92
|
+
when (cachePolicy) {
|
|
93
|
+
GraniteImageCachePolicy.NONE -> {
|
|
94
|
+
memoryCachePolicy(CachePolicy.DISABLED)
|
|
95
|
+
diskCachePolicy(CachePolicy.DISABLED)
|
|
96
|
+
}
|
|
97
|
+
GraniteImageCachePolicy.MEMORY -> {
|
|
98
|
+
memoryCachePolicy(CachePolicy.ENABLED)
|
|
99
|
+
diskCachePolicy(CachePolicy.DISABLED)
|
|
110
100
|
}
|
|
101
|
+
GraniteImageCachePolicy.DISK -> {
|
|
102
|
+
memoryCachePolicy(CachePolicy.ENABLED)
|
|
103
|
+
diskCachePolicy(CachePolicy.ENABLED)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
111
107
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
onSuccess = { _, result ->
|
|
117
|
-
val bitmap = (result.drawable as? android.graphics.drawable.BitmapDrawable)?.bitmap
|
|
118
|
-
val width = bitmap?.width ?: 0
|
|
119
|
-
val height = bitmap?.height ?: 0
|
|
120
|
-
Log.d(TAG, "Loaded with Coil: $url")
|
|
121
|
-
completionCallback?.invoke(bitmap, null, width, height)
|
|
122
|
-
},
|
|
123
|
-
onError = { _, result ->
|
|
124
|
-
Log.e(TAG, "Error loading image: ${result.throwable.message}")
|
|
125
|
-
completionCallback?.invoke(null, result.throwable as? Exception, 0, 0)
|
|
126
|
-
}
|
|
127
|
-
)
|
|
108
|
+
private fun ImageRequest.Builder.applyPlaceholder(context: Context, defaultSource: String?) {
|
|
109
|
+
if (!defaultSource.isNullOrEmpty()) {
|
|
110
|
+
val resourceId = context.resources.getIdentifier(defaultSource, "drawable", context.packageName)
|
|
111
|
+
if (resourceId != 0) placeholder(resourceId)
|
|
128
112
|
}
|
|
129
113
|
}
|
|
130
114
|
|
|
115
|
+
private fun handleSuccess(
|
|
116
|
+
result: SuccessResult,
|
|
117
|
+
url: String,
|
|
118
|
+
completionCallback: GraniteImageCompletionCallback?
|
|
119
|
+
) {
|
|
120
|
+
val bitmap = (result.drawable as? BitmapDrawable)?.bitmap
|
|
121
|
+
Log.d(TAG, "Loaded with Coil: $url")
|
|
122
|
+
completionCallback?.invoke(bitmap, null, bitmap?.width ?: 0, bitmap?.height ?: 0)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private fun handleError(result: ErrorResult, completionCallback: GraniteImageCompletionCallback?) {
|
|
126
|
+
Log.e(TAG, "Error loading image: ${result.throwable.message}")
|
|
127
|
+
completionCallback?.invoke(null, result.throwable as? Exception, 0, 0)
|
|
128
|
+
}
|
|
129
|
+
|
|
131
130
|
override fun cancelLoad(view: View) {
|
|
132
131
|
if (view is ImageView) {
|
|
133
132
|
view.load(null as String?)
|
|
@@ -153,4 +152,18 @@ class CoilImageProvider : GraniteImageProvider {
|
|
|
153
152
|
// Coil preload is not directly supported without a context
|
|
154
153
|
onCompletion?.invoke(false, 0, 0, "Preload not supported without context")
|
|
155
154
|
}
|
|
155
|
+
|
|
156
|
+
override fun clearMemoryCache(context: Context) {
|
|
157
|
+
Coil.imageLoader(context).memoryCache?.clear()
|
|
158
|
+
Log.d(TAG, "Memory cache cleared (Coil 2.x)")
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
override fun clearDiskCache(context: Context) {
|
|
162
|
+
// Coil 2.x: no public API for diskCache — no-op
|
|
163
|
+
Log.d(TAG, "Disk cache clear not supported (Coil 2.x)")
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
companion object {
|
|
167
|
+
private const val TAG = "CoilImageProvider"
|
|
168
|
+
}
|
|
156
169
|
}
|
|
@@ -4,15 +4,11 @@ import android.content.Context
|
|
|
4
4
|
import android.graphics.Bitmap
|
|
5
5
|
import android.graphics.PorterDuff
|
|
6
6
|
import android.graphics.PorterDuffColorFilter
|
|
7
|
-
import android.
|
|
7
|
+
import android.os.Handler
|
|
8
|
+
import android.os.Looper
|
|
8
9
|
import android.util.Log
|
|
9
10
|
import android.view.View
|
|
10
11
|
import android.widget.ImageView
|
|
11
|
-
import run.granite.image.GraniteImageProvider
|
|
12
|
-
import run.granite.image.GraniteImagePriority
|
|
13
|
-
import run.granite.image.GraniteImageCachePolicy
|
|
14
|
-
import run.granite.image.GraniteImageProgressCallback
|
|
15
|
-
import run.granite.image.GraniteImageCompletionCallback
|
|
16
12
|
import com.bumptech.glide.Glide
|
|
17
13
|
import com.bumptech.glide.load.DataSource
|
|
18
14
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|
@@ -22,21 +18,17 @@ import com.bumptech.glide.load.model.LazyHeaders
|
|
|
22
18
|
import com.bumptech.glide.request.RequestListener
|
|
23
19
|
import com.bumptech.glide.request.target.Target
|
|
24
20
|
import com.bumptech.glide.Priority as GlidePriority
|
|
21
|
+
import run.granite.image.GraniteImageProvider
|
|
22
|
+
import run.granite.image.GraniteImagePriority
|
|
23
|
+
import run.granite.image.GraniteImageCachePolicy
|
|
24
|
+
import run.granite.image.GraniteImageProgressCallback
|
|
25
|
+
import run.granite.image.GraniteImageCompletionCallback
|
|
26
|
+
import java.util.concurrent.Executors
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* GraniteImageProvider implementation using Glide.
|
|
28
30
|
*/
|
|
29
31
|
class GlideImageProvider : GraniteImageProvider {
|
|
30
|
-
companion object {
|
|
31
|
-
private const val TAG = "GlideImageProvider"
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
override fun createImageView(context: Context): View {
|
|
35
|
-
return ImageView(context).apply {
|
|
36
|
-
setBackgroundColor(android.graphics.Color.LTGRAY)
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
32
|
override fun loadImage(url: String, into: View, scaleType: ImageView.ScaleType) {
|
|
41
33
|
loadImage(url, into, scaleType, null, GraniteImagePriority.NORMAL, GraniteImageCachePolicy.DISK, null, null, null)
|
|
42
34
|
}
|
|
@@ -52,91 +44,94 @@ class GlideImageProvider : GraniteImageProvider {
|
|
|
52
44
|
progressCallback: GraniteImageProgressCallback?,
|
|
53
45
|
completionCallback: GraniteImageCompletionCallback?
|
|
54
46
|
) {
|
|
55
|
-
val imageView
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return
|
|
60
|
-
}
|
|
61
|
-
into.scaleType = scaleType
|
|
62
|
-
into
|
|
63
|
-
} else {
|
|
64
|
-
null
|
|
47
|
+
val imageView = validateImageView(into, scaleType, completionCallback) ?: return
|
|
48
|
+
val context = imageView.context ?: run {
|
|
49
|
+
completionCallback?.invoke(null, Exception("Glide requires a view context for image loading"), 0, 0)
|
|
50
|
+
return
|
|
65
51
|
}
|
|
66
52
|
|
|
67
|
-
|
|
68
|
-
val
|
|
69
|
-
|
|
70
|
-
headers.forEach { (key, value) ->
|
|
71
|
-
headersBuilder.addHeader(key, value)
|
|
72
|
-
}
|
|
73
|
-
GlideUrl(url, headersBuilder.build())
|
|
74
|
-
} else {
|
|
75
|
-
GlideUrl(url)
|
|
76
|
-
}
|
|
53
|
+
val glideUrl = buildGlideUrl(url, headers)
|
|
54
|
+
val requestBuilder = configureRequest(context, glideUrl, priority, cachePolicy, defaultSource)
|
|
55
|
+
.listener(createRequestListener(url, completionCallback))
|
|
77
56
|
|
|
78
|
-
|
|
57
|
+
requestBuilder.into(imageView)
|
|
58
|
+
}
|
|
79
59
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
60
|
+
private fun validateImageView(
|
|
61
|
+
into: View?,
|
|
62
|
+
scaleType: ImageView.ScaleType,
|
|
63
|
+
completionCallback: GraniteImageCompletionCallback?
|
|
64
|
+
): ImageView? {
|
|
65
|
+
if (into == null) return null
|
|
66
|
+
if (into !is ImageView) {
|
|
67
|
+
Log.e(TAG, "View is not an ImageView")
|
|
68
|
+
completionCallback?.invoke(null, Exception("View is not an ImageView"), 0, 0)
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
into.scaleType = scaleType
|
|
72
|
+
return into
|
|
73
|
+
}
|
|
84
74
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
75
|
+
private fun buildGlideUrl(url: String, headers: Map<String, String>?): GlideUrl {
|
|
76
|
+
if (headers.isNullOrEmpty()) return GlideUrl(url)
|
|
77
|
+
val headersBuilder = LazyHeaders.Builder()
|
|
78
|
+
headers.forEach { (key, value) -> headersBuilder.addHeader(key, value) }
|
|
79
|
+
return GlideUrl(url, headersBuilder.build())
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private fun configureRequest(
|
|
83
|
+
context: Context,
|
|
84
|
+
glideUrl: GlideUrl,
|
|
85
|
+
priority: GraniteImagePriority,
|
|
86
|
+
cachePolicy: GraniteImageCachePolicy,
|
|
87
|
+
defaultSource: String?
|
|
88
|
+
): com.bumptech.glide.RequestBuilder<Bitmap> {
|
|
89
|
+
var builder = Glide.with(context).asBitmap().load(glideUrl)
|
|
90
|
+
|
|
91
|
+
builder = when (priority) {
|
|
92
|
+
GraniteImagePriority.LOW -> builder.priority(GlidePriority.LOW)
|
|
93
|
+
GraniteImagePriority.NORMAL -> builder.priority(GlidePriority.NORMAL)
|
|
94
|
+
GraniteImagePriority.HIGH -> builder.priority(GlidePriority.HIGH)
|
|
90
95
|
}
|
|
91
96
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
GraniteImageCachePolicy.
|
|
95
|
-
GraniteImageCachePolicy.
|
|
96
|
-
GraniteImageCachePolicy.DISK -> requestBuilder.diskCacheStrategy(DiskCacheStrategy.ALL)
|
|
97
|
+
builder = when (cachePolicy) {
|
|
98
|
+
GraniteImageCachePolicy.NONE -> builder.diskCacheStrategy(DiskCacheStrategy.NONE).skipMemoryCache(true)
|
|
99
|
+
GraniteImageCachePolicy.MEMORY -> builder.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
100
|
+
GraniteImageCachePolicy.DISK -> builder.diskCacheStrategy(DiskCacheStrategy.ALL)
|
|
97
101
|
}
|
|
98
102
|
|
|
99
|
-
// Apply default source (placeholder)
|
|
100
103
|
if (!defaultSource.isNullOrEmpty()) {
|
|
101
|
-
// Try to load as drawable resource first, then as URL
|
|
102
104
|
val resourceId = context.resources.getIdentifier(defaultSource, "drawable", context.packageName)
|
|
103
|
-
if (resourceId != 0)
|
|
104
|
-
requestBuilder = requestBuilder.placeholder(resourceId)
|
|
105
|
-
}
|
|
105
|
+
if (resourceId != 0) builder = builder.placeholder(resourceId)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
override fun onLoadFailed(
|
|
111
|
-
e: GlideException?,
|
|
112
|
-
model: Any?,
|
|
113
|
-
target: Target<Bitmap>,
|
|
114
|
-
isFirstResource: Boolean
|
|
115
|
-
): Boolean {
|
|
116
|
-
Log.e(TAG, "Error loading image: ${e?.message}")
|
|
117
|
-
completionCallback?.invoke(null, e, 0, 0)
|
|
118
|
-
return false
|
|
119
|
-
}
|
|
108
|
+
return builder
|
|
109
|
+
}
|
|
120
110
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
Log.d(TAG, "Loaded with Glide ($cacheTypeStr): $url")
|
|
134
|
-
completionCallback?.invoke(resource, null, resource.width, resource.height)
|
|
135
|
-
return false
|
|
136
|
-
}
|
|
137
|
-
})
|
|
111
|
+
private fun createRequestListener(
|
|
112
|
+
url: String,
|
|
113
|
+
completionCallback: GraniteImageCompletionCallback?
|
|
114
|
+
): RequestListener<Bitmap> = object : RequestListener<Bitmap> {
|
|
115
|
+
override fun onLoadFailed(
|
|
116
|
+
e: GlideException?, model: Any?, target: Target<Bitmap>, isFirstResource: Boolean
|
|
117
|
+
): Boolean {
|
|
118
|
+
Log.e(TAG, "Error loading image: ${e?.message}")
|
|
119
|
+
completionCallback?.invoke(null, e, 0, 0)
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
138
122
|
|
|
139
|
-
|
|
123
|
+
override fun onResourceReady(
|
|
124
|
+
resource: Bitmap, model: Any, target: Target<Bitmap>?, dataSource: DataSource, isFirstResource: Boolean
|
|
125
|
+
): Boolean {
|
|
126
|
+
val cacheType = when (dataSource) {
|
|
127
|
+
DataSource.MEMORY_CACHE -> "Memory"
|
|
128
|
+
DataSource.DATA_DISK_CACHE, DataSource.RESOURCE_DISK_CACHE -> "Disk"
|
|
129
|
+
else -> "Network"
|
|
130
|
+
}
|
|
131
|
+
Log.d(TAG, "Loaded with Glide ($cacheType): $url")
|
|
132
|
+
completionCallback?.invoke(resource, null, resource.width, resource.height)
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
140
135
|
}
|
|
141
136
|
|
|
142
137
|
override fun cancelLoad(view: View) {
|
|
@@ -165,4 +160,32 @@ class GlideImageProvider : GraniteImageProvider {
|
|
|
165
160
|
// This would need an Application context passed during initialization
|
|
166
161
|
onCompletion?.invoke(false, 0, 0, "Preload not supported without context")
|
|
167
162
|
}
|
|
163
|
+
|
|
164
|
+
override fun clearMemoryCache(context: Context) {
|
|
165
|
+
// Glide: clearMemory() must be called on the main thread
|
|
166
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
167
|
+
Glide.get(context).clearMemory()
|
|
168
|
+
} else {
|
|
169
|
+
Handler(Looper.getMainLooper()).post {
|
|
170
|
+
Glide.get(context).clearMemory()
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
Log.d(TAG, "Memory cache cleared (Glide)")
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
override fun clearDiskCache(context: Context) {
|
|
177
|
+
// Glide: clearDiskCache() must be called on a background thread
|
|
178
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
179
|
+
Glide.get(context).clearDiskCache()
|
|
180
|
+
} else {
|
|
181
|
+
Executors.newSingleThreadExecutor().execute {
|
|
182
|
+
Glide.get(context).clearDiskCache()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
Log.d(TAG, "Disk cache cleared (Glide)")
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
companion object {
|
|
189
|
+
private const val TAG = "GlideImageProvider"
|
|
190
|
+
}
|
|
168
191
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package run.granite.image
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.graphics.Bitmap
|
|
4
5
|
import android.graphics.Color
|
|
5
6
|
import android.graphics.PorterDuff
|
|
6
7
|
import android.graphics.PorterDuffColorFilter
|
|
@@ -10,11 +11,9 @@ import android.view.View
|
|
|
10
11
|
import android.widget.FrameLayout
|
|
11
12
|
import android.widget.ImageView
|
|
12
13
|
import android.widget.TextView
|
|
13
|
-
import com.facebook.react.bridge.Arguments
|
|
14
14
|
import com.facebook.react.bridge.ReactContext
|
|
15
|
-
import com.facebook.react.bridge.WritableMap
|
|
16
15
|
import com.facebook.react.uimanager.UIManagerHelper
|
|
17
|
-
import com.facebook.react.uimanager.events.
|
|
16
|
+
import com.facebook.react.uimanager.events.Event
|
|
18
17
|
import org.json.JSONObject
|
|
19
18
|
|
|
20
19
|
/**
|
|
@@ -22,9 +21,7 @@ import org.json.JSONObject
|
|
|
22
21
|
* It delegates actual image loading to the registered GraniteImageProvider.
|
|
23
22
|
*/
|
|
24
23
|
class GraniteImage(context: Context) : FrameLayout(context) {
|
|
25
|
-
|
|
26
|
-
private const val TAG = "GraniteImage"
|
|
27
|
-
}
|
|
24
|
+
internal var providerResolver: () -> GraniteImageProvider? = { GraniteImageRegistry.provider }
|
|
28
25
|
|
|
29
26
|
private var containerView: View? = null
|
|
30
27
|
private var currentUri: String? = null
|
|
@@ -42,46 +39,25 @@ class GraniteImage(context: Context) : FrameLayout(context) {
|
|
|
42
39
|
Log.d(TAG, "GraniteImage initialized")
|
|
43
40
|
}
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
|
|
42
|
+
private fun dispatchEvent(event: Event<*>) {
|
|
43
|
+
val reactContext = context as? ReactContext ?: return
|
|
44
|
+
UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)?.dispatchEvent(event)
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
private fun emitLoadStart()
|
|
52
|
-
|
|
53
|
-
getEventDispatcher()?.dispatchEvent(
|
|
54
|
-
GraniteImageLoadStartEvent(UIManagerHelper.getSurfaceId(this), id)
|
|
55
|
-
)
|
|
56
|
-
}
|
|
47
|
+
private fun emitLoadStart() =
|
|
48
|
+
dispatchEvent(GraniteImageLoadStartEvent(UIManagerHelper.getSurfaceId(this), id))
|
|
57
49
|
|
|
58
|
-
private fun emitProgress(loaded: Int, total: Int)
|
|
59
|
-
|
|
60
|
-
getEventDispatcher()?.dispatchEvent(
|
|
61
|
-
GraniteImageProgressEvent(UIManagerHelper.getSurfaceId(this), id, loaded, total)
|
|
62
|
-
)
|
|
63
|
-
}
|
|
50
|
+
private fun emitProgress(loaded: Int, total: Int) =
|
|
51
|
+
dispatchEvent(GraniteImageProgressEvent(UIManagerHelper.getSurfaceId(this), id, loaded, total))
|
|
64
52
|
|
|
65
|
-
private fun emitLoad(width: Int, height: Int)
|
|
66
|
-
|
|
67
|
-
getEventDispatcher()?.dispatchEvent(
|
|
68
|
-
GraniteImageLoadEvent(UIManagerHelper.getSurfaceId(this), id, width, height)
|
|
69
|
-
)
|
|
70
|
-
}
|
|
53
|
+
private fun emitLoad(width: Int, height: Int) =
|
|
54
|
+
dispatchEvent(GraniteImageLoadEvent(UIManagerHelper.getSurfaceId(this), id, width, height))
|
|
71
55
|
|
|
72
|
-
private fun emitError(error: String)
|
|
73
|
-
|
|
74
|
-
getEventDispatcher()?.dispatchEvent(
|
|
75
|
-
GraniteImageErrorEvent(UIManagerHelper.getSurfaceId(this), id, error)
|
|
76
|
-
)
|
|
77
|
-
}
|
|
56
|
+
private fun emitError(error: String) =
|
|
57
|
+
dispatchEvent(GraniteImageErrorEvent(UIManagerHelper.getSurfaceId(this), id, error))
|
|
78
58
|
|
|
79
|
-
private fun emitLoadEnd()
|
|
80
|
-
|
|
81
|
-
getEventDispatcher()?.dispatchEvent(
|
|
82
|
-
GraniteImageLoadEndEvent(UIManagerHelper.getSurfaceId(this), id)
|
|
83
|
-
)
|
|
84
|
-
}
|
|
59
|
+
private fun emitLoadEnd() =
|
|
60
|
+
dispatchEvent(GraniteImageLoadEndEvent(UIManagerHelper.getSurfaceId(this), id))
|
|
85
61
|
|
|
86
62
|
fun setUri(uri: String?) {
|
|
87
63
|
if (uri != currentUri) {
|
|
@@ -123,14 +99,11 @@ class GraniteImage(context: Context) : FrameLayout(context) {
|
|
|
123
99
|
}
|
|
124
100
|
|
|
125
101
|
fun setPriority(priority: String?) {
|
|
126
|
-
currentPriority =
|
|
127
|
-
"low" -> GraniteImagePriority.LOW
|
|
128
|
-
"high" -> GraniteImagePriority.HIGH
|
|
129
|
-
else -> GraniteImagePriority.NORMAL
|
|
130
|
-
}
|
|
102
|
+
currentPriority = GraniteImagePriority.fromString(priority)
|
|
131
103
|
}
|
|
132
104
|
|
|
133
105
|
fun setCachePolicy(cachePolicy: String?) {
|
|
106
|
+
// View-side API uses canonical names: "memory", "none", "disk" (default)
|
|
134
107
|
currentCachePolicy = when (cachePolicy) {
|
|
135
108
|
"memory" -> GraniteImageCachePolicy.MEMORY
|
|
136
109
|
"none" -> GraniteImageCachePolicy.NONE
|
|
@@ -163,7 +136,7 @@ class GraniteImage(context: Context) : FrameLayout(context) {
|
|
|
163
136
|
containerView = null
|
|
164
137
|
}
|
|
165
138
|
|
|
166
|
-
val provider =
|
|
139
|
+
val provider = providerResolver()
|
|
167
140
|
|
|
168
141
|
if (provider == null) {
|
|
169
142
|
showErrorView("No GraniteImageProvider registered")
|
|
@@ -206,37 +179,48 @@ class GraniteImage(context: Context) : FrameLayout(context) {
|
|
|
206
179
|
},
|
|
207
180
|
completionCallback = { bitmap, error, width, height ->
|
|
208
181
|
post {
|
|
209
|
-
|
|
210
|
-
emitError(error.message ?: "Unknown error")
|
|
211
|
-
|
|
212
|
-
// Load fallback image if available
|
|
213
|
-
currentFallbackSource?.let { fallback ->
|
|
214
|
-
provider.loadImage(
|
|
215
|
-
url = fallback,
|
|
216
|
-
into = imageView,
|
|
217
|
-
scaleType = currentScaleType,
|
|
218
|
-
headers = null,
|
|
219
|
-
priority = GraniteImagePriority.HIGH,
|
|
220
|
-
cachePolicy = GraniteImageCachePolicy.DISK,
|
|
221
|
-
defaultSource = null,
|
|
222
|
-
progressCallback = null,
|
|
223
|
-
completionCallback = null
|
|
224
|
-
)
|
|
225
|
-
}
|
|
226
|
-
} else {
|
|
227
|
-
emitLoad(width, height)
|
|
228
|
-
|
|
229
|
-
// Apply tint color if set
|
|
230
|
-
currentTintColor?.let { color ->
|
|
231
|
-
provider.applyTintColor(color, imageView)
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
emitLoadEnd()
|
|
182
|
+
handleLoadCompletion(bitmap, error, width, height, imageView, provider)
|
|
235
183
|
}
|
|
236
184
|
}
|
|
237
185
|
)
|
|
238
186
|
}
|
|
239
187
|
|
|
188
|
+
private fun handleLoadCompletion(
|
|
189
|
+
bitmap: Bitmap?,
|
|
190
|
+
error: Exception?,
|
|
191
|
+
width: Int,
|
|
192
|
+
height: Int,
|
|
193
|
+
imageView: View,
|
|
194
|
+
provider: GraniteImageProvider
|
|
195
|
+
) {
|
|
196
|
+
if (error != null) {
|
|
197
|
+
emitError(error.message ?: "Unknown error")
|
|
198
|
+
|
|
199
|
+
// Load fallback image if available
|
|
200
|
+
currentFallbackSource?.let { fallback ->
|
|
201
|
+
provider.loadImage(
|
|
202
|
+
url = fallback,
|
|
203
|
+
into = imageView,
|
|
204
|
+
scaleType = currentScaleType,
|
|
205
|
+
headers = null,
|
|
206
|
+
priority = GraniteImagePriority.HIGH,
|
|
207
|
+
cachePolicy = GraniteImageCachePolicy.DISK,
|
|
208
|
+
defaultSource = null,
|
|
209
|
+
progressCallback = null,
|
|
210
|
+
completionCallback = null
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
emitLoad(width, height)
|
|
215
|
+
|
|
216
|
+
// Apply tint color if set
|
|
217
|
+
currentTintColor?.let { color ->
|
|
218
|
+
provider.applyTintColor(color, imageView)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
emitLoadEnd()
|
|
222
|
+
}
|
|
223
|
+
|
|
240
224
|
private fun showErrorView(message: String) {
|
|
241
225
|
Log.e(TAG, message)
|
|
242
226
|
|
|
@@ -266,7 +250,7 @@ class GraniteImage(context: Context) : FrameLayout(context) {
|
|
|
266
250
|
}
|
|
267
251
|
|
|
268
252
|
fun cleanup() {
|
|
269
|
-
val provider =
|
|
253
|
+
val provider = providerResolver()
|
|
270
254
|
containerView?.let {
|
|
271
255
|
provider?.cancelLoad(it)
|
|
272
256
|
removeView(it)
|
|
@@ -274,4 +258,8 @@ class GraniteImage(context: Context) : FrameLayout(context) {
|
|
|
274
258
|
containerView = null
|
|
275
259
|
currentUri = null
|
|
276
260
|
}
|
|
261
|
+
|
|
262
|
+
companion object {
|
|
263
|
+
private const val TAG = "GraniteImage"
|
|
264
|
+
}
|
|
277
265
|
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
package run.granite.image
|
|
2
2
|
|
|
3
3
|
import android.util.Log
|
|
4
|
-
import com.facebook.react.bridge.ReadableMap
|
|
5
4
|
import com.facebook.react.common.MapBuilder
|
|
6
5
|
import com.facebook.react.module.annotations.ReactModule
|
|
7
6
|
import com.facebook.react.uimanager.SimpleViewManager
|
|
8
7
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
9
8
|
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
10
9
|
import com.facebook.react.uimanager.annotations.ReactProp
|
|
11
|
-
import com.facebook.react.uimanager.events.EventDispatcher
|
|
12
10
|
import com.facebook.react.viewmanagers.GraniteImageManagerDelegate
|
|
13
11
|
import com.facebook.react.viewmanagers.GraniteImageManagerInterface
|
|
14
12
|
|
|
@@ -18,11 +16,6 @@ import com.facebook.react.viewmanagers.GraniteImageManagerInterface
|
|
|
18
16
|
*/
|
|
19
17
|
@ReactModule(name = GraniteImageManager.NAME)
|
|
20
18
|
class GraniteImageManager : SimpleViewManager<GraniteImage>(), GraniteImageManagerInterface<GraniteImage> {
|
|
21
|
-
companion object {
|
|
22
|
-
const val NAME = "GraniteImage"
|
|
23
|
-
private const val TAG = "GraniteImageManager"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
19
|
private val delegate: ViewManagerDelegate<GraniteImage> = GraniteImageManagerDelegate(this)
|
|
27
20
|
|
|
28
21
|
override fun getName(): String = NAME
|
|
@@ -97,4 +90,9 @@ class GraniteImageManager : SimpleViewManager<GraniteImage>(), GraniteImageManag
|
|
|
97
90
|
.put("onGraniteLoadEnd", MapBuilder.of("registrationName", "onGraniteLoadEnd"))
|
|
98
91
|
.build()
|
|
99
92
|
}
|
|
93
|
+
|
|
94
|
+
companion object {
|
|
95
|
+
const val NAME = "GraniteImage"
|
|
96
|
+
private const val TAG = "GraniteImageManager"
|
|
97
|
+
}
|
|
100
98
|
}
|
|
@@ -8,18 +8,23 @@ import com.facebook.react.bridge.ReactMethod
|
|
|
8
8
|
import com.facebook.react.module.annotations.ReactModule
|
|
9
9
|
import org.json.JSONArray
|
|
10
10
|
import org.json.JSONObject
|
|
11
|
+
import java.util.concurrent.ExecutorService
|
|
11
12
|
import java.util.concurrent.Executors
|
|
12
13
|
import java.util.concurrent.atomic.AtomicInteger
|
|
13
14
|
|
|
14
15
|
@ReactModule(name = GraniteImageModule.NAME)
|
|
15
|
-
class GraniteImageModule(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
class GraniteImageModule(
|
|
17
|
+
reactContext: ReactApplicationContext,
|
|
18
|
+
private val providerResolver: () -> GraniteImageProvider? = { GraniteImageRegistry.provider },
|
|
19
|
+
private val executor: ExecutorService = Executors.newFixedThreadPool(4)
|
|
20
|
+
) : ReactContextBaseJavaModule(reactContext) {
|
|
21
|
+
|
|
22
|
+
private data class PreloadSource(
|
|
23
|
+
val uri: String,
|
|
24
|
+
val headers: Map<String, String>?,
|
|
25
|
+
val priority: GraniteImagePriority,
|
|
26
|
+
val cachePolicy: GraniteImageCachePolicy
|
|
27
|
+
)
|
|
23
28
|
|
|
24
29
|
override fun getName(): String = NAME
|
|
25
30
|
|
|
@@ -27,7 +32,7 @@ class GraniteImageModule(reactContext: ReactApplicationContext) : ReactContextBa
|
|
|
27
32
|
fun preload(sourcesJson: String, promise: Promise) {
|
|
28
33
|
Log.d(TAG, "preload called with: $sourcesJson")
|
|
29
34
|
|
|
30
|
-
val provider =
|
|
35
|
+
val provider = providerResolver()
|
|
31
36
|
if (provider == null) {
|
|
32
37
|
Log.w(TAG, "No provider registered, cannot preload")
|
|
33
38
|
promise.reject("NO_PROVIDER", "No provider registered, cannot preload")
|
|
@@ -38,75 +43,19 @@ class GraniteImageModule(reactContext: ReactApplicationContext) : ReactContextBa
|
|
|
38
43
|
try {
|
|
39
44
|
val sources = JSONArray(sourcesJson)
|
|
40
45
|
val totalCount = sources.length()
|
|
41
|
-
val completedCount = AtomicInteger(0)
|
|
42
|
-
val successCount = AtomicInteger(0)
|
|
43
|
-
val failCount = AtomicInteger(0)
|
|
44
46
|
|
|
45
47
|
if (totalCount == 0) {
|
|
46
48
|
promise.resolve(null)
|
|
47
49
|
return@execute
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
continue
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
val headersObj = source.optJSONObject("headers")
|
|
62
|
-
val headers = mutableMapOf<String, String>()
|
|
63
|
-
headersObj?.let {
|
|
64
|
-
val keys = it.keys()
|
|
65
|
-
while (keys.hasNext()) {
|
|
66
|
-
val key = keys.next()
|
|
67
|
-
headers[key] = it.getString(key)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
val priorityStr = source.optString("priority", "normal")
|
|
72
|
-
val priority = when (priorityStr) {
|
|
73
|
-
"high" -> GraniteImagePriority.HIGH
|
|
74
|
-
"low" -> GraniteImagePriority.LOW
|
|
75
|
-
else -> GraniteImagePriority.NORMAL
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
val cacheStr = source.optString("cache", "")
|
|
79
|
-
val cachePolicy = when (cacheStr) {
|
|
80
|
-
"cacheOnly" -> GraniteImageCachePolicy.DISK
|
|
81
|
-
"web" -> GraniteImageCachePolicy.NONE
|
|
82
|
-
else -> GraniteImageCachePolicy.DISK
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
Log.d(TAG, "Preloading: $uri")
|
|
86
|
-
|
|
87
|
-
// Call provider's preload method (loadImage with null view for preload)
|
|
88
|
-
provider.loadImage(
|
|
89
|
-
url = uri,
|
|
90
|
-
imageView = null,
|
|
91
|
-
contentMode = "cover",
|
|
92
|
-
headers = headers.ifEmpty { null },
|
|
93
|
-
priority = priority,
|
|
94
|
-
cachePolicy = cachePolicy,
|
|
95
|
-
onProgress = null,
|
|
96
|
-
onCompletion = { success, width, height, error ->
|
|
97
|
-
if (success) {
|
|
98
|
-
Log.d(TAG, "Preloaded successfully: $uri (${width}x${height})")
|
|
99
|
-
successCount.incrementAndGet()
|
|
100
|
-
} else {
|
|
101
|
-
Log.d(TAG, "Preload failed for $uri: $error")
|
|
102
|
-
failCount.incrementAndGet()
|
|
103
|
-
}
|
|
104
|
-
if (completedCount.incrementAndGet() == totalCount) {
|
|
105
|
-
Log.d(TAG, "Preload completed: ${successCount.get()} succeeded, ${failCount.get()} failed")
|
|
106
|
-
promise.resolve(null)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
)
|
|
52
|
+
val completedCount = AtomicInteger(0)
|
|
53
|
+
val successCount = AtomicInteger(0)
|
|
54
|
+
val failCount = AtomicInteger(0)
|
|
55
|
+
|
|
56
|
+
for (i in 0 until totalCount) {
|
|
57
|
+
val preloadSource = parsePreloadSource(sources.getJSONObject(i))
|
|
58
|
+
preloadSingle(provider, preloadSource, completedCount, successCount, failCount, totalCount, promise)
|
|
110
59
|
}
|
|
111
60
|
} catch (e: Exception) {
|
|
112
61
|
Log.e(TAG, "Failed to parse sources JSON: ${e.message}")
|
|
@@ -115,17 +64,112 @@ class GraniteImageModule(reactContext: ReactApplicationContext) : ReactContextBa
|
|
|
115
64
|
}
|
|
116
65
|
}
|
|
117
66
|
|
|
67
|
+
private fun preloadSingle(
|
|
68
|
+
provider: GraniteImageProvider,
|
|
69
|
+
source: PreloadSource,
|
|
70
|
+
completedCount: AtomicInteger,
|
|
71
|
+
successCount: AtomicInteger,
|
|
72
|
+
failCount: AtomicInteger,
|
|
73
|
+
totalCount: Int,
|
|
74
|
+
promise: Promise
|
|
75
|
+
) {
|
|
76
|
+
if (source.uri.isEmpty()) {
|
|
77
|
+
checkPreloadCompletion(completedCount, successCount, failCount, totalCount, promise)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Log.d(TAG, "Preloading: ${source.uri}")
|
|
82
|
+
provider.loadImage(
|
|
83
|
+
url = source.uri,
|
|
84
|
+
imageView = null,
|
|
85
|
+
contentMode = "cover",
|
|
86
|
+
headers = source.headers,
|
|
87
|
+
priority = source.priority,
|
|
88
|
+
cachePolicy = source.cachePolicy,
|
|
89
|
+
onProgress = null,
|
|
90
|
+
onCompletion = { success, width, height, error ->
|
|
91
|
+
if (success) {
|
|
92
|
+
Log.d(TAG, "Preloaded successfully: ${source.uri} (${width}x${height})")
|
|
93
|
+
successCount.incrementAndGet()
|
|
94
|
+
} else {
|
|
95
|
+
Log.d(TAG, "Preload failed for ${source.uri}: $error")
|
|
96
|
+
failCount.incrementAndGet()
|
|
97
|
+
}
|
|
98
|
+
checkPreloadCompletion(completedCount, successCount, failCount, totalCount, promise)
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private fun checkPreloadCompletion(
|
|
104
|
+
completedCount: AtomicInteger,
|
|
105
|
+
successCount: AtomicInteger,
|
|
106
|
+
failCount: AtomicInteger,
|
|
107
|
+
totalCount: Int,
|
|
108
|
+
promise: Promise
|
|
109
|
+
) {
|
|
110
|
+
if (completedCount.incrementAndGet() == totalCount) {
|
|
111
|
+
Log.d(TAG, "Preload completed: ${successCount.get()} succeeded, ${failCount.get()} failed")
|
|
112
|
+
promise.resolve(null)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private fun parsePreloadSource(source: JSONObject): PreloadSource {
|
|
117
|
+
val uri = source.optString("uri", "")
|
|
118
|
+
|
|
119
|
+
val headersObj = source.optJSONObject("headers")
|
|
120
|
+
val headers = mutableMapOf<String, String>()
|
|
121
|
+
headersObj?.let {
|
|
122
|
+
val keys = it.keys()
|
|
123
|
+
while (keys.hasNext()) {
|
|
124
|
+
val key = keys.next()
|
|
125
|
+
headers[key] = it.getString(key)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
val priorityStr = source.optString("priority", "normal")
|
|
130
|
+
val priority = GraniteImagePriority.fromString(priorityStr)
|
|
131
|
+
|
|
132
|
+
// Module-side API uses FastImage-compatible names: "cacheOnly", "web"
|
|
133
|
+
// (intentionally different from View-side API which uses "memory", "none")
|
|
134
|
+
val cacheStr = source.optString("cache", "")
|
|
135
|
+
val cachePolicy = when (cacheStr) {
|
|
136
|
+
"cacheOnly" -> GraniteImageCachePolicy.DISK
|
|
137
|
+
"web" -> GraniteImageCachePolicy.NONE
|
|
138
|
+
else -> GraniteImageCachePolicy.DISK
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return PreloadSource(
|
|
142
|
+
uri = uri,
|
|
143
|
+
headers = headers.ifEmpty { null },
|
|
144
|
+
priority = priority,
|
|
145
|
+
cachePolicy = cachePolicy
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
118
149
|
@ReactMethod
|
|
119
150
|
fun clearMemoryCache(promise: Promise) {
|
|
120
151
|
Log.d(TAG, "clearMemoryCache called")
|
|
121
|
-
|
|
152
|
+
val provider = providerResolver()
|
|
153
|
+
val context = reactApplicationContext
|
|
154
|
+
if (provider != null) {
|
|
155
|
+
provider.clearMemoryCache(context)
|
|
156
|
+
}
|
|
122
157
|
promise.resolve(null)
|
|
123
158
|
}
|
|
124
159
|
|
|
125
160
|
@ReactMethod
|
|
126
161
|
fun clearDiskCache(promise: Promise) {
|
|
127
162
|
Log.d(TAG, "clearDiskCache called")
|
|
128
|
-
|
|
163
|
+
val provider = providerResolver()
|
|
164
|
+
val context = reactApplicationContext
|
|
165
|
+
if (provider != null) {
|
|
166
|
+
provider.clearDiskCache(context)
|
|
167
|
+
}
|
|
129
168
|
promise.resolve(null)
|
|
130
169
|
}
|
|
170
|
+
|
|
171
|
+
companion object {
|
|
172
|
+
const val NAME = "GraniteImageModule"
|
|
173
|
+
private const val TAG = "GraniteImageModule"
|
|
174
|
+
}
|
|
131
175
|
}
|
|
@@ -10,15 +10,21 @@ import com.facebook.react.uimanager.ViewManager
|
|
|
10
10
|
* React Native Package that registers the GraniteImage component and GraniteImageModule.
|
|
11
11
|
*/
|
|
12
12
|
class GraniteImagePackage : ReactPackage {
|
|
13
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
14
|
+
ensureProviderRegistered()
|
|
15
|
+
return listOf(GraniteImageModule(reactContext))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
19
|
+
ensureProviderRegistered()
|
|
20
|
+
return listOf(GraniteImageManager())
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
companion object {
|
|
14
24
|
private const val TAG = "GraniteImagePackage"
|
|
15
25
|
private var providerRegistered = false
|
|
16
26
|
|
|
17
|
-
|
|
18
|
-
registerDefaultProvider()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
private fun registerDefaultProvider() {
|
|
27
|
+
internal fun ensureProviderRegistered() {
|
|
22
28
|
if (providerRegistered) return
|
|
23
29
|
|
|
24
30
|
// Try to auto-register a provider based on available implementations
|
|
@@ -48,12 +54,4 @@ class GraniteImagePackage : ReactPackage {
|
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
|
-
|
|
52
|
-
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
53
|
-
return listOf(GraniteImageModule(reactContext))
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
57
|
-
return listOf(GraniteImageManager())
|
|
58
|
-
}
|
|
59
57
|
}
|
|
@@ -2,6 +2,7 @@ package run.granite.image
|
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.graphics.Bitmap
|
|
5
|
+
import android.graphics.Color
|
|
5
6
|
import android.view.View
|
|
6
7
|
import android.widget.ImageView
|
|
7
8
|
|
|
@@ -9,9 +10,18 @@ import android.widget.ImageView
|
|
|
9
10
|
* Priority levels for image loading
|
|
10
11
|
*/
|
|
11
12
|
enum class GraniteImagePriority {
|
|
12
|
-
LOW, NORMAL, HIGH
|
|
13
|
+
LOW, NORMAL, HIGH;
|
|
14
|
+
|
|
15
|
+
companion object {
|
|
16
|
+
fun fromString(value: String?): GraniteImagePriority = when (value) {
|
|
17
|
+
"low" -> LOW
|
|
18
|
+
"high" -> HIGH
|
|
19
|
+
else -> NORMAL
|
|
20
|
+
}
|
|
21
|
+
}
|
|
13
22
|
}
|
|
14
23
|
|
|
24
|
+
|
|
15
25
|
/**
|
|
16
26
|
* Cache policy for image loading
|
|
17
27
|
*/
|
|
@@ -37,8 +47,13 @@ interface GraniteImageProvider {
|
|
|
37
47
|
/**
|
|
38
48
|
* Creates and returns a new View that will be used to display the image.
|
|
39
49
|
* The returned view should be capable of displaying images (typically ImageView).
|
|
50
|
+
* Default implementation creates an ImageView with a light gray background.
|
|
40
51
|
*/
|
|
41
|
-
fun createImageView(context: Context): View
|
|
52
|
+
fun createImageView(context: Context): View {
|
|
53
|
+
return ImageView(context).apply {
|
|
54
|
+
setBackgroundColor(Color.LTGRAY)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
42
57
|
|
|
43
58
|
/**
|
|
44
59
|
* Loads an image from the given URL into the provided view.
|
|
@@ -102,4 +117,20 @@ interface GraniteImageProvider {
|
|
|
102
117
|
fun applyTintColor(color: Int, view: View) {
|
|
103
118
|
// Default implementation - do nothing
|
|
104
119
|
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Clears the memory cache.
|
|
123
|
+
* Default implementation is no-op. Override in provider implementations to clear actual cache.
|
|
124
|
+
*/
|
|
125
|
+
fun clearMemoryCache(context: Context) {
|
|
126
|
+
// default: no-op
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Clears the disk cache.
|
|
131
|
+
* Default implementation is no-op. Override in provider implementations to clear actual cache.
|
|
132
|
+
*/
|
|
133
|
+
fun clearDiskCache(context: Context) {
|
|
134
|
+
// default: no-op
|
|
135
|
+
}
|
|
105
136
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
package run.granite.image.providers
|
|
2
2
|
|
|
3
|
-
import android.content.Context
|
|
4
3
|
import android.graphics.Bitmap
|
|
5
4
|
import android.graphics.BitmapFactory
|
|
6
5
|
import android.graphics.PorterDuff
|
|
@@ -21,8 +20,9 @@ import okhttp3.CacheControl
|
|
|
21
20
|
import okhttp3.OkHttpClient
|
|
22
21
|
import okhttp3.Request
|
|
23
22
|
import okhttp3.Response
|
|
23
|
+
import java.io.ByteArrayOutputStream
|
|
24
24
|
import java.io.IOException
|
|
25
|
-
import java.util.
|
|
25
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
26
26
|
import java.util.concurrent.TimeUnit
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -30,23 +30,13 @@ import java.util.concurrent.TimeUnit
|
|
|
30
30
|
* This is analogous to iOS's URLSessionImageProvider.
|
|
31
31
|
*/
|
|
32
32
|
class OkHttpImageProvider : GraniteImageProvider {
|
|
33
|
-
companion object {
|
|
34
|
-
private const val TAG = "OkHttpImageProvider"
|
|
35
|
-
}
|
|
36
|
-
|
|
37
33
|
private val client = OkHttpClient.Builder()
|
|
38
34
|
.connectTimeout(30, TimeUnit.SECONDS)
|
|
39
35
|
.readTimeout(30, TimeUnit.SECONDS)
|
|
40
36
|
.build()
|
|
41
|
-
private val activeCalls =
|
|
37
|
+
private val activeCalls = ConcurrentHashMap<View, Call>()
|
|
42
38
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
43
39
|
|
|
44
|
-
override fun createImageView(context: Context): View {
|
|
45
|
-
return ImageView(context).apply {
|
|
46
|
-
setBackgroundColor(android.graphics.Color.LTGRAY)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
40
|
override fun loadImage(url: String, into: View, scaleType: ImageView.ScaleType) {
|
|
51
41
|
loadImage(url, into, scaleType, null, GraniteImagePriority.NORMAL, GraniteImageCachePolicy.DISK, null, null, null)
|
|
52
42
|
}
|
|
@@ -85,24 +75,7 @@ class OkHttpImageProvider : GraniteImageProvider {
|
|
|
85
75
|
}
|
|
86
76
|
}
|
|
87
77
|
|
|
88
|
-
val
|
|
89
|
-
|
|
90
|
-
// Add headers
|
|
91
|
-
headers?.forEach { (key, value) ->
|
|
92
|
-
requestBuilder.addHeader(key, value)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Apply cache policy
|
|
96
|
-
when (cachePolicy) {
|
|
97
|
-
GraniteImageCachePolicy.NONE -> {
|
|
98
|
-
requestBuilder.cacheControl(CacheControl.FORCE_NETWORK)
|
|
99
|
-
}
|
|
100
|
-
GraniteImageCachePolicy.MEMORY, GraniteImageCachePolicy.DISK -> {
|
|
101
|
-
// Use default caching
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
val request = requestBuilder.build()
|
|
78
|
+
val request = buildRequest(url, headers, cachePolicy)
|
|
106
79
|
val call = client.newCall(request)
|
|
107
80
|
if (into != null) {
|
|
108
81
|
activeCalls[into] = call
|
|
@@ -120,73 +93,102 @@ class OkHttpImageProvider : GraniteImageProvider {
|
|
|
120
93
|
}
|
|
121
94
|
|
|
122
95
|
override fun onResponse(call: Call, response: Response) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (!response.isSuccessful) {
|
|
128
|
-
val error = Exception("HTTP error: ${response.code}")
|
|
129
|
-
Log.e(TAG, "HTTP error: ${response.code}")
|
|
130
|
-
mainHandler.post {
|
|
131
|
-
completionCallback?.invoke(null, error, 0, 0)
|
|
96
|
+
response.use {
|
|
97
|
+
if (into != null) {
|
|
98
|
+
activeCalls.remove(into)
|
|
132
99
|
}
|
|
133
|
-
|
|
100
|
+
handleResponse(response, imageView, url, progressCallback, completionCallback)
|
|
134
101
|
}
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
|
135
105
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
return
|
|
144
|
-
}
|
|
106
|
+
private fun buildRequest(
|
|
107
|
+
url: String,
|
|
108
|
+
headers: Map<String, String>?,
|
|
109
|
+
cachePolicy: GraniteImageCachePolicy
|
|
110
|
+
): Request {
|
|
111
|
+
val requestBuilder = Request.Builder().url(url)
|
|
145
112
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
val inputStream = body.byteStream()
|
|
151
|
-
val bytes = ByteArray(contentLength.toInt().coerceAtLeast(1024))
|
|
152
|
-
var totalBytesRead = 0L
|
|
153
|
-
val buffer = ByteArray(8192)
|
|
154
|
-
var bytesRead: Int
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
val outputStream = java.io.ByteArrayOutputStream()
|
|
158
|
-
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
|
159
|
-
outputStream.write(buffer, 0, bytesRead)
|
|
160
|
-
totalBytesRead += bytesRead
|
|
161
|
-
if (contentLength > 0) {
|
|
162
|
-
progressCallback?.invoke(totalBytesRead, contentLength)
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
val imageBytes = outputStream.toByteArray()
|
|
166
|
-
|
|
167
|
-
val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
|
168
|
-
if (bitmap == null) {
|
|
169
|
-
val error = Exception("Failed to decode image data")
|
|
170
|
-
Log.e(TAG, "Failed to decode image data")
|
|
171
|
-
mainHandler.post {
|
|
172
|
-
completionCallback?.invoke(null, error, 0, 0)
|
|
173
|
-
}
|
|
174
|
-
return
|
|
175
|
-
}
|
|
113
|
+
// Add headers
|
|
114
|
+
headers?.forEach { (key, value) ->
|
|
115
|
+
requestBuilder.addHeader(key, value)
|
|
116
|
+
}
|
|
176
117
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
} catch (e: Exception) {
|
|
183
|
-
Log.e(TAG, "Error reading image data: ${e.message}")
|
|
184
|
-
mainHandler.post {
|
|
185
|
-
completionCallback?.invoke(null, e, 0, 0)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
118
|
+
// Apply cache policy
|
|
119
|
+
when (cachePolicy) {
|
|
120
|
+
GraniteImageCachePolicy.NONE -> {
|
|
121
|
+
requestBuilder.cacheControl(CacheControl.FORCE_NETWORK)
|
|
188
122
|
}
|
|
189
|
-
|
|
123
|
+
GraniteImageCachePolicy.MEMORY, GraniteImageCachePolicy.DISK -> {
|
|
124
|
+
// Use default caching
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return requestBuilder.build()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private fun handleResponse(
|
|
132
|
+
response: Response,
|
|
133
|
+
imageView: ImageView?,
|
|
134
|
+
url: String,
|
|
135
|
+
progressCallback: GraniteImageProgressCallback?,
|
|
136
|
+
completionCallback: GraniteImageCompletionCallback?
|
|
137
|
+
) {
|
|
138
|
+
if (!response.isSuccessful) {
|
|
139
|
+
postError(Exception("HTTP error: ${response.code}"), completionCallback)
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
val body = response.body
|
|
144
|
+
if (body == null) {
|
|
145
|
+
postError(Exception("No data received"), completionCallback)
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
val imageBytes = readResponseBytes(body, progressCallback)
|
|
151
|
+
val bitmap = decodeBitmap(imageBytes)
|
|
152
|
+
if (bitmap == null) {
|
|
153
|
+
postError(Exception("Failed to decode image data"), completionCallback)
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
mainHandler.post {
|
|
157
|
+
imageView?.setImageBitmap(bitmap)
|
|
158
|
+
Log.d(TAG, "Loaded with OkHttp: $url")
|
|
159
|
+
completionCallback?.invoke(bitmap, null, bitmap.width, bitmap.height)
|
|
160
|
+
}
|
|
161
|
+
} catch (e: Exception) {
|
|
162
|
+
Log.e(TAG, "Error reading image data: ${e.message}")
|
|
163
|
+
mainHandler.post { completionCallback?.invoke(null, e, 0, 0) }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private fun readResponseBytes(
|
|
168
|
+
body: okhttp3.ResponseBody,
|
|
169
|
+
progressCallback: GraniteImageProgressCallback?
|
|
170
|
+
): ByteArray {
|
|
171
|
+
val contentLength = body.contentLength()
|
|
172
|
+
val inputStream = body.byteStream()
|
|
173
|
+
val outputStream = ByteArrayOutputStream()
|
|
174
|
+
val buffer = ByteArray(8192)
|
|
175
|
+
var totalBytesRead = 0L
|
|
176
|
+
var bytesRead: Int
|
|
177
|
+
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
|
178
|
+
outputStream.write(buffer, 0, bytesRead)
|
|
179
|
+
totalBytesRead += bytesRead
|
|
180
|
+
if (contentLength > 0) progressCallback?.invoke(totalBytesRead, contentLength)
|
|
181
|
+
}
|
|
182
|
+
return outputStream.toByteArray()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private fun decodeBitmap(imageBytes: ByteArray): Bitmap? {
|
|
186
|
+
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private fun postError(error: Exception, completionCallback: GraniteImageCompletionCallback?) {
|
|
190
|
+
Log.e(TAG, error.message ?: "Unknown error")
|
|
191
|
+
mainHandler.post { completionCallback?.invoke(null, error, 0, 0) }
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
override fun cancelLoad(view: View) {
|
|
@@ -225,4 +227,23 @@ class OkHttpImageProvider : GraniteImageProvider {
|
|
|
225
227
|
}
|
|
226
228
|
)
|
|
227
229
|
}
|
|
230
|
+
|
|
231
|
+
override fun clearMemoryCache(context: Context) {
|
|
232
|
+
// OkHttp: no built-in memory cache — no-op
|
|
233
|
+
Log.d(TAG, "Memory cache clear not supported (OkHttp)")
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
override fun clearDiskCache(context: Context) {
|
|
237
|
+
// OkHttp: evict all if client.cache is configured
|
|
238
|
+
try {
|
|
239
|
+
client.cache?.evictAll()
|
|
240
|
+
Log.d(TAG, "Disk cache cleared (OkHttp)")
|
|
241
|
+
} catch (e: Exception) {
|
|
242
|
+
Log.e(TAG, "Failed to clear disk cache: ${e.message}")
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
companion object {
|
|
247
|
+
private const val TAG = "OkHttpImageProvider"
|
|
248
|
+
}
|
|
228
249
|
}
|
package/package.json
CHANGED