stream-chat-react-native 5.44.2 → 6.0.0-beta.1
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/README.md +1 -2
- package/android/build.gradle +103 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/com/streamchatreactnative/StreamChatReactNative.java +594 -0
- package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativeModule.java +111 -0
- package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java +45 -0
- package/android/src/newarch/com/streamchatreactnative/StreamChatReactNative.java +9 -0
- package/android/src/oldarch/com/streamchatreactnative/StreamChatReactNative.java +16 -0
- package/ios/ImageHelpers.h +57 -0
- package/ios/ImageHelpers.m +179 -0
- package/ios/StreamChatReactNative.h +13 -0
- package/ios/StreamChatReactNative.mm +417 -0
- package/ios/StreamChatReactNative.xcodeproj/project.pbxproj +280 -0
- package/package.json +31 -21
- package/src/handlers/compressImage.ts +3 -4
- package/src/handlers/index.ts +0 -5
- package/src/index.js +6 -3
- package/src/native/NativeStreamChatReactNative.ts +24 -0
- package/src/native/index.tsx +48 -0
- package/src/native/types.ts +32 -0
- package/src/optionalDependencies/Audio.ts +10 -4
- package/src/optionalDependencies/FlatList.ts +21 -0
- package/src/{handlers → optionalDependencies}/Sound.tsx +1 -1
- package/src/{handlers → optionalDependencies}/Video.tsx +1 -1
- package/src/optionalDependencies/deleteFile.ts +19 -0
- package/src/optionalDependencies/getLocalAssetUri.ts +8 -3
- package/src/optionalDependencies/getPhotos.ts +20 -10
- package/src/optionalDependencies/iOS14RefreshGallerySelection.ts +1 -1
- package/src/optionalDependencies/index.ts +10 -6
- package/src/optionalDependencies/oniOS14GalleryLibrarySelectionChange.ts +1 -1
- package/src/optionalDependencies/saveFile.ts +24 -0
- package/src/optionalDependencies/shareImage.ts +10 -6
- package/src/optionalDependencies/takePhoto.ts +15 -11
- package/stream-chat-react-native.podspec +38 -0
- package/src/handlers/NetInfo.ts +0 -43
- package/src/handlers/deleteFile.ts +0 -11
- package/src/handlers/saveFile.ts +0 -11
- /package/src/optionalDependencies/{Video.ts → AudioVideo.ts} +0 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
package com.streamchatreactnative;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.content.ContentResolver;
|
|
5
|
+
import android.database.Cursor;
|
|
6
|
+
import android.graphics.Bitmap;
|
|
7
|
+
import android.graphics.BitmapFactory;
|
|
8
|
+
import android.graphics.Matrix;
|
|
9
|
+
import androidx.exifinterface.media.ExifInterface;
|
|
10
|
+
import android.net.Uri;
|
|
11
|
+
import android.os.Build;
|
|
12
|
+
import android.provider.MediaStore;
|
|
13
|
+
import android.util.Base64;
|
|
14
|
+
import android.util.Log;
|
|
15
|
+
|
|
16
|
+
import java.io.ByteArrayOutputStream;
|
|
17
|
+
import java.io.File;
|
|
18
|
+
import java.io.FileOutputStream;
|
|
19
|
+
import java.io.InputStream;
|
|
20
|
+
import java.io.IOException;
|
|
21
|
+
import java.net.HttpURLConnection;
|
|
22
|
+
import java.net.URL;
|
|
23
|
+
import java.util.Date;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Provide methods to resize and rotate an image file.
|
|
27
|
+
*/
|
|
28
|
+
public class StreamChatReactNative {
|
|
29
|
+
private final static String IMAGE_JPEG = "image/jpeg";
|
|
30
|
+
private final static String IMAGE_PNG = "image/png";
|
|
31
|
+
private final static String SCHEME_DATA = "data";
|
|
32
|
+
private final static String SCHEME_CONTENT = "content";
|
|
33
|
+
private final static String SCHEME_FILE = "file";
|
|
34
|
+
private final static String SCHEME_HTTP = "http";
|
|
35
|
+
private final static String SCHEME_HTTPS = "https";
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
// List of known EXIF tags we will be copying.
|
|
39
|
+
// Orientation, width, height, and some others are ignored
|
|
40
|
+
// TODO: Find any missing tag that might be useful
|
|
41
|
+
private final static String[] EXIF_TO_COPY_ROTATED = new String[]
|
|
42
|
+
{
|
|
43
|
+
ExifInterface.TAG_APERTURE_VALUE,
|
|
44
|
+
ExifInterface.TAG_MAX_APERTURE_VALUE,
|
|
45
|
+
ExifInterface.TAG_METERING_MODE,
|
|
46
|
+
ExifInterface.TAG_ARTIST,
|
|
47
|
+
ExifInterface.TAG_BITS_PER_SAMPLE,
|
|
48
|
+
ExifInterface.TAG_COMPRESSION,
|
|
49
|
+
ExifInterface.TAG_BODY_SERIAL_NUMBER,
|
|
50
|
+
ExifInterface.TAG_BRIGHTNESS_VALUE,
|
|
51
|
+
ExifInterface.TAG_CONTRAST,
|
|
52
|
+
ExifInterface.TAG_CAMERA_OWNER_NAME,
|
|
53
|
+
ExifInterface.TAG_COLOR_SPACE,
|
|
54
|
+
ExifInterface.TAG_COPYRIGHT,
|
|
55
|
+
ExifInterface.TAG_DATETIME,
|
|
56
|
+
ExifInterface.TAG_DATETIME_DIGITIZED,
|
|
57
|
+
ExifInterface.TAG_DATETIME_ORIGINAL,
|
|
58
|
+
ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
|
|
59
|
+
ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
|
|
60
|
+
ExifInterface.TAG_EXIF_VERSION,
|
|
61
|
+
ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
|
|
62
|
+
ExifInterface.TAG_EXPOSURE_INDEX,
|
|
63
|
+
ExifInterface.TAG_EXPOSURE_MODE,
|
|
64
|
+
ExifInterface.TAG_EXPOSURE_TIME,
|
|
65
|
+
ExifInterface.TAG_EXPOSURE_PROGRAM,
|
|
66
|
+
ExifInterface.TAG_FLASH,
|
|
67
|
+
ExifInterface.TAG_FLASH_ENERGY,
|
|
68
|
+
ExifInterface.TAG_FOCAL_LENGTH,
|
|
69
|
+
ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
|
|
70
|
+
ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
|
|
71
|
+
ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
|
|
72
|
+
ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
|
|
73
|
+
ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION,
|
|
74
|
+
ExifInterface.TAG_PLANAR_CONFIGURATION,
|
|
75
|
+
ExifInterface.TAG_F_NUMBER,
|
|
76
|
+
ExifInterface.TAG_GAIN_CONTROL,
|
|
77
|
+
ExifInterface.TAG_GAMMA,
|
|
78
|
+
ExifInterface.TAG_GPS_ALTITUDE,
|
|
79
|
+
ExifInterface.TAG_GPS_ALTITUDE_REF,
|
|
80
|
+
ExifInterface.TAG_GPS_AREA_INFORMATION,
|
|
81
|
+
ExifInterface.TAG_GPS_DATESTAMP,
|
|
82
|
+
ExifInterface.TAG_GPS_DOP,
|
|
83
|
+
ExifInterface.TAG_GPS_LATITUDE,
|
|
84
|
+
ExifInterface.TAG_GPS_LATITUDE_REF,
|
|
85
|
+
ExifInterface.TAG_GPS_LONGITUDE,
|
|
86
|
+
ExifInterface.TAG_GPS_LONGITUDE_REF,
|
|
87
|
+
ExifInterface.TAG_GPS_STATUS,
|
|
88
|
+
ExifInterface.TAG_GPS_DEST_BEARING,
|
|
89
|
+
ExifInterface.TAG_GPS_DEST_BEARING_REF,
|
|
90
|
+
ExifInterface.TAG_GPS_DEST_DISTANCE,
|
|
91
|
+
ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
|
|
92
|
+
ExifInterface.TAG_GPS_DEST_LATITUDE,
|
|
93
|
+
ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
|
|
94
|
+
ExifInterface.TAG_GPS_DEST_LONGITUDE,
|
|
95
|
+
ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
|
|
96
|
+
ExifInterface.TAG_GPS_DIFFERENTIAL,
|
|
97
|
+
ExifInterface.TAG_GPS_IMG_DIRECTION,
|
|
98
|
+
ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
|
|
99
|
+
ExifInterface.TAG_GPS_MAP_DATUM,
|
|
100
|
+
ExifInterface.TAG_GPS_MEASURE_MODE,
|
|
101
|
+
ExifInterface.TAG_GPS_PROCESSING_METHOD,
|
|
102
|
+
ExifInterface.TAG_GPS_SATELLITES,
|
|
103
|
+
ExifInterface.TAG_GPS_SPEED,
|
|
104
|
+
ExifInterface.TAG_GPS_SPEED_REF,
|
|
105
|
+
ExifInterface.TAG_GPS_STATUS,
|
|
106
|
+
ExifInterface.TAG_GPS_TIMESTAMP,
|
|
107
|
+
ExifInterface.TAG_GPS_TRACK,
|
|
108
|
+
ExifInterface.TAG_GPS_TRACK_REF,
|
|
109
|
+
ExifInterface.TAG_GPS_VERSION_ID,
|
|
110
|
+
ExifInterface.TAG_IMAGE_DESCRIPTION,
|
|
111
|
+
ExifInterface.TAG_IMAGE_UNIQUE_ID,
|
|
112
|
+
ExifInterface.TAG_ISO_SPEED,
|
|
113
|
+
ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY,
|
|
114
|
+
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
|
|
115
|
+
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
|
|
116
|
+
ExifInterface.TAG_LENS_MAKE,
|
|
117
|
+
ExifInterface.TAG_LENS_MODEL,
|
|
118
|
+
ExifInterface.TAG_LENS_SERIAL_NUMBER,
|
|
119
|
+
ExifInterface.TAG_LENS_SPECIFICATION,
|
|
120
|
+
ExifInterface.TAG_LIGHT_SOURCE,
|
|
121
|
+
ExifInterface.TAG_MAKE,
|
|
122
|
+
ExifInterface.TAG_MAKER_NOTE,
|
|
123
|
+
ExifInterface.TAG_MODEL,
|
|
124
|
+
// ExifInterface.TAG_ORIENTATION, // removed
|
|
125
|
+
ExifInterface.TAG_SATURATION,
|
|
126
|
+
ExifInterface.TAG_SHARPNESS,
|
|
127
|
+
ExifInterface.TAG_SHUTTER_SPEED_VALUE,
|
|
128
|
+
ExifInterface.TAG_SOFTWARE,
|
|
129
|
+
ExifInterface.TAG_SUBJECT_DISTANCE,
|
|
130
|
+
ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
|
|
131
|
+
ExifInterface.TAG_SUBJECT_LOCATION,
|
|
132
|
+
ExifInterface.TAG_USER_COMMENT,
|
|
133
|
+
ExifInterface.TAG_WHITE_BALANCE
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Resize the specified bitmap.
|
|
140
|
+
*/
|
|
141
|
+
private static Bitmap resizeImage(Bitmap image, int newWidth, int newHeight,
|
|
142
|
+
String mode, boolean onlyScaleDown) {
|
|
143
|
+
Bitmap newImage = null;
|
|
144
|
+
if (image == null) {
|
|
145
|
+
return null; // Can't load the image from the given path.
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
int width = image.getWidth();
|
|
149
|
+
int height = image.getHeight();
|
|
150
|
+
|
|
151
|
+
if (newHeight > 0 && newWidth > 0) {
|
|
152
|
+
int finalWidth;
|
|
153
|
+
int finalHeight;
|
|
154
|
+
|
|
155
|
+
if (mode.equals("stretch")) {
|
|
156
|
+
// Distort aspect ratio
|
|
157
|
+
finalWidth = newWidth;
|
|
158
|
+
finalHeight = newHeight;
|
|
159
|
+
|
|
160
|
+
if (onlyScaleDown) {
|
|
161
|
+
finalWidth = Math.min(width, finalWidth);
|
|
162
|
+
finalHeight = Math.min(height, finalHeight);
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
// "contain" (default) or "cover": keep its aspect ratio
|
|
166
|
+
float widthRatio = (float) newWidth / width;
|
|
167
|
+
float heightRatio = (float) newHeight / height;
|
|
168
|
+
|
|
169
|
+
float ratio = mode.equals("cover") ?
|
|
170
|
+
Math.max(widthRatio, heightRatio) :
|
|
171
|
+
Math.min(widthRatio, heightRatio);
|
|
172
|
+
|
|
173
|
+
if (onlyScaleDown) ratio = Math.min(ratio, 1);
|
|
174
|
+
|
|
175
|
+
finalWidth = (int) Math.round(width * ratio);
|
|
176
|
+
finalHeight = (int) Math.round(height * ratio);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
newImage = Bitmap.createScaledBitmap(image, finalWidth, finalHeight, true);
|
|
181
|
+
} catch (OutOfMemoryError e) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return newImage;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Rotate the specified bitmap with the given angle, in degrees.
|
|
191
|
+
*/
|
|
192
|
+
public static Bitmap rotateImage(Bitmap source, Matrix matrix, float angle)
|
|
193
|
+
{
|
|
194
|
+
Bitmap retVal;
|
|
195
|
+
matrix.postRotate(angle);
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
retVal = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true);
|
|
199
|
+
} catch (OutOfMemoryError e) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
return retVal;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Save the given bitmap in a directory. Extension is automatically generated using the bitmap format.
|
|
207
|
+
*/
|
|
208
|
+
public static File saveImage(Bitmap bitmap, File saveDirectory, String fileName,
|
|
209
|
+
Bitmap.CompressFormat compressFormat, int quality)
|
|
210
|
+
throws IOException {
|
|
211
|
+
if (bitmap == null) {
|
|
212
|
+
throw new IOException("The bitmap couldn't be resized");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
File newFile = new File(saveDirectory, fileName + "." + compressFormat.name());
|
|
216
|
+
if(!newFile.createNewFile()) {
|
|
217
|
+
throw new IOException("The file already exists");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
221
|
+
bitmap.compress(compressFormat, quality, outputStream);
|
|
222
|
+
byte[] bitmapData = outputStream.toByteArray();
|
|
223
|
+
|
|
224
|
+
outputStream.flush();
|
|
225
|
+
outputStream.close();
|
|
226
|
+
|
|
227
|
+
FileOutputStream fos = new FileOutputStream(newFile);
|
|
228
|
+
fos.write(bitmapData);
|
|
229
|
+
fos.flush();
|
|
230
|
+
fos.close();
|
|
231
|
+
|
|
232
|
+
return newFile;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get {@link File} object for the given Android URI.<br>
|
|
237
|
+
* Use content resolver to get real path if direct path doesn't return valid file.
|
|
238
|
+
*/
|
|
239
|
+
private static File getFileFromUri(Context context, Uri uri) {
|
|
240
|
+
|
|
241
|
+
// first try by direct path
|
|
242
|
+
File file = new File(uri.getPath());
|
|
243
|
+
if (file.exists()) {
|
|
244
|
+
return file;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// try reading real path from content resolver (gallery images)
|
|
248
|
+
Cursor cursor = null;
|
|
249
|
+
try {
|
|
250
|
+
String[] proj = {MediaStore.Images.Media.DATA};
|
|
251
|
+
cursor = context.getContentResolver().query(uri, proj, null, null, null);
|
|
252
|
+
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
|
|
253
|
+
cursor.moveToFirst();
|
|
254
|
+
String realPath = cursor.getString(column_index);
|
|
255
|
+
file = new File(realPath);
|
|
256
|
+
} catch (Exception ignored) {
|
|
257
|
+
} finally {
|
|
258
|
+
if (cursor != null) {
|
|
259
|
+
cursor.close();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return file;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Attempts to copy exif info from one file to another. Note: orientation, width, and height
|
|
268
|
+
exif attributes are not copied since those are lost after image rotation.
|
|
269
|
+
|
|
270
|
+
* imageUri: original image URI as provided from JS
|
|
271
|
+
* dstPath: final image output path
|
|
272
|
+
* Returns true if copy was successful, false otherwise.
|
|
273
|
+
*/
|
|
274
|
+
public static boolean copyExif(Context context, Uri imageUri, String dstPath){
|
|
275
|
+
ExifInterface src = null;
|
|
276
|
+
ExifInterface dst = null;
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
|
|
280
|
+
File file = getFileFromUri(context, imageUri);
|
|
281
|
+
if (!file.exists()) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
src = new ExifInterface(file.getAbsolutePath());
|
|
286
|
+
dst = new ExifInterface(dstPath);
|
|
287
|
+
|
|
288
|
+
} catch (Exception ignored) {
|
|
289
|
+
Log.e("StreamChatReactNative::copyExif", "EXIF read failed", ignored);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if(src == null || dst == null){
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try{
|
|
297
|
+
|
|
298
|
+
for (String attr : EXIF_TO_COPY_ROTATED)
|
|
299
|
+
{
|
|
300
|
+
String value = src.getAttribute(attr);
|
|
301
|
+
if (value != null){
|
|
302
|
+
dst.setAttribute(attr, value);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
dst.saveAttributes();
|
|
306
|
+
|
|
307
|
+
} catch (Exception ignored) {
|
|
308
|
+
Log.e("StreamChatReactNative::copyExif", "EXIF copy failed", ignored);
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Get orientation by reading Image metadata
|
|
317
|
+
*/
|
|
318
|
+
public static Matrix getOrientationMatrix(Context context, Uri uri) {
|
|
319
|
+
try {
|
|
320
|
+
// ExifInterface(InputStream) only exists since Android N (r24)
|
|
321
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
322
|
+
InputStream input = context.getContentResolver().openInputStream(uri);
|
|
323
|
+
ExifInterface ei = new ExifInterface(input);
|
|
324
|
+
return getOrientationMatrix(ei);
|
|
325
|
+
}
|
|
326
|
+
File file = getFileFromUri(context, uri);
|
|
327
|
+
if (file.exists()) {
|
|
328
|
+
ExifInterface ei = new ExifInterface(file.getAbsolutePath());
|
|
329
|
+
return getOrientationMatrix(ei);
|
|
330
|
+
}
|
|
331
|
+
} catch (Exception ignored) { }
|
|
332
|
+
|
|
333
|
+
return new Matrix();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Convert metadata to degrees
|
|
338
|
+
*/
|
|
339
|
+
public static Matrix getOrientationMatrix(ExifInterface exif) {
|
|
340
|
+
Matrix matrix = new Matrix();
|
|
341
|
+
int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
|
|
342
|
+
switch (orientation) {
|
|
343
|
+
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
|
|
344
|
+
matrix.setScale(-1, 1);
|
|
345
|
+
break;
|
|
346
|
+
case ExifInterface.ORIENTATION_TRANSPOSE:
|
|
347
|
+
matrix.setRotate(90);
|
|
348
|
+
matrix.postScale(-1, 1);
|
|
349
|
+
break;
|
|
350
|
+
case ExifInterface.ORIENTATION_ROTATE_90:
|
|
351
|
+
matrix.setRotate(90);
|
|
352
|
+
break;
|
|
353
|
+
case ExifInterface.ORIENTATION_FLIP_VERTICAL:
|
|
354
|
+
matrix.setRotate(180);
|
|
355
|
+
matrix.postScale(-1, 1);
|
|
356
|
+
break;
|
|
357
|
+
case ExifInterface.ORIENTATION_ROTATE_180:
|
|
358
|
+
matrix.setRotate(180);
|
|
359
|
+
break;
|
|
360
|
+
case ExifInterface.ORIENTATION_TRANSVERSE:
|
|
361
|
+
matrix.setRotate(270);
|
|
362
|
+
matrix.postScale(-1, 1);
|
|
363
|
+
break;
|
|
364
|
+
case ExifInterface.ORIENTATION_ROTATE_270:
|
|
365
|
+
matrix.setRotate(270);
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
return matrix;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Compute the inSampleSize value to use to load a bitmap.
|
|
373
|
+
* Adapted from https://developer.android.com/training/displaying-bitmaps/load-bitmap.html
|
|
374
|
+
*/
|
|
375
|
+
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
|
|
376
|
+
final int height = options.outHeight;
|
|
377
|
+
final int width = options.outWidth;
|
|
378
|
+
|
|
379
|
+
int inSampleSize = 1;
|
|
380
|
+
|
|
381
|
+
if (height > reqHeight || width > reqWidth) {
|
|
382
|
+
final int halfHeight = height / 2;
|
|
383
|
+
final int halfWidth = width / 2;
|
|
384
|
+
|
|
385
|
+
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
|
|
386
|
+
// height and width larger than the requested height and width.
|
|
387
|
+
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
|
|
388
|
+
inSampleSize *= 2;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return inSampleSize;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Load a bitmap either from a real file or using the {@link ContentResolver} of the current
|
|
397
|
+
* {@link Context} (to read gallery images for example).
|
|
398
|
+
*
|
|
399
|
+
* Note that, when options.inJustDecodeBounds = true, we actually expect sourceImage to remain
|
|
400
|
+
* as null (see https://developer.android.com/training/displaying-bitmaps/load-bitmap.html), so
|
|
401
|
+
* getting null sourceImage at the completion of this method is not always worthy of an error.
|
|
402
|
+
*/
|
|
403
|
+
private static Bitmap loadBitmap(Context context, Uri imageUri, BitmapFactory.Options options) throws IOException {
|
|
404
|
+
Bitmap sourceImage = null;
|
|
405
|
+
String imageUriScheme = imageUri.getScheme();
|
|
406
|
+
if (imageUriScheme == null || !imageUriScheme.equalsIgnoreCase(SCHEME_CONTENT)) {
|
|
407
|
+
try {
|
|
408
|
+
sourceImage = BitmapFactory.decodeFile(imageUri.getPath(), options);
|
|
409
|
+
} catch (Exception e) {
|
|
410
|
+
e.printStackTrace();
|
|
411
|
+
throw new IOException("Error decoding image file");
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
ContentResolver cr = context.getContentResolver();
|
|
415
|
+
InputStream input = cr.openInputStream(imageUri);
|
|
416
|
+
if (input != null) {
|
|
417
|
+
sourceImage = BitmapFactory.decodeStream(input, null, options);
|
|
418
|
+
input.close();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return sourceImage;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Loads the bitmap resource from the file specified in imagePath.
|
|
426
|
+
*/
|
|
427
|
+
private static Bitmap loadBitmapFromFile(Context context, Uri imageUri, int newWidth,
|
|
428
|
+
int newHeight) throws IOException {
|
|
429
|
+
// Decode the image bounds to find the size of the source image.
|
|
430
|
+
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
431
|
+
options.inJustDecodeBounds = true;
|
|
432
|
+
loadBitmap(context, imageUri, options);
|
|
433
|
+
|
|
434
|
+
// Set a sample size according to the image size to lower memory usage.
|
|
435
|
+
options.inSampleSize = calculateInSampleSize(options, newWidth, newHeight);
|
|
436
|
+
options.inJustDecodeBounds = false;
|
|
437
|
+
//System.out.println(options.inSampleSize);
|
|
438
|
+
return loadBitmap(context, imageUri, options);
|
|
439
|
+
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Loads the bitmap resource from an URL
|
|
444
|
+
*/
|
|
445
|
+
private static Bitmap loadBitmapFromURL(Uri imageUri, int newWidth,
|
|
446
|
+
int newHeight) throws IOException {
|
|
447
|
+
|
|
448
|
+
InputStream input = null;
|
|
449
|
+
Bitmap sourceImage = null;
|
|
450
|
+
|
|
451
|
+
try{
|
|
452
|
+
URL url = new URL(imageUri.toString());
|
|
453
|
+
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
|
454
|
+
connection.connect();
|
|
455
|
+
input = connection.getInputStream();
|
|
456
|
+
|
|
457
|
+
if (input != null) {
|
|
458
|
+
|
|
459
|
+
// need to load into memory since inputstream is not seekable
|
|
460
|
+
// we still won't load the whole bitmap into memory
|
|
461
|
+
// Also need this ugly code since we are on Java8...
|
|
462
|
+
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
|
463
|
+
int nRead;
|
|
464
|
+
byte[] data = new byte[1024];
|
|
465
|
+
byte[] imageData = null;
|
|
466
|
+
|
|
467
|
+
try{
|
|
468
|
+
while ((nRead = input.read(data, 0, data.length)) != -1) {
|
|
469
|
+
buffer.write(data, 0, nRead);
|
|
470
|
+
}
|
|
471
|
+
buffer.flush();
|
|
472
|
+
imageData = buffer.toByteArray();
|
|
473
|
+
}
|
|
474
|
+
finally{
|
|
475
|
+
buffer.close();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
// Decode the image bounds to find the size of the source image.
|
|
480
|
+
// Do it here so we only do one request
|
|
481
|
+
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
482
|
+
options.inJustDecodeBounds = true;
|
|
483
|
+
BitmapFactory.decodeByteArray(imageData, 0, imageData.length, options);
|
|
484
|
+
|
|
485
|
+
// Set a sample size according to the image size to lower memory usage.
|
|
486
|
+
options.inSampleSize = calculateInSampleSize(options, newWidth, newHeight);
|
|
487
|
+
options.inJustDecodeBounds = false;
|
|
488
|
+
|
|
489
|
+
sourceImage = BitmapFactory.decodeByteArray(imageData, 0, imageData.length, options);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
catch (Exception e) {
|
|
493
|
+
e.printStackTrace();
|
|
494
|
+
throw new IOException("Error fetching remote image file.");
|
|
495
|
+
}
|
|
496
|
+
finally{
|
|
497
|
+
try {
|
|
498
|
+
if(input != null){
|
|
499
|
+
input.close();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
catch (IOException e) {
|
|
503
|
+
e.printStackTrace();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return sourceImage;
|
|
509
|
+
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Loads the bitmap resource from a base64 encoded jpg or png.
|
|
514
|
+
* Format is as such:
|
|
515
|
+
* png: 'data:image/png;base64,iVBORw0KGgoAA...'
|
|
516
|
+
* jpg: 'data:image/jpeg;base64,/9j/4AAQSkZJ...'
|
|
517
|
+
*/
|
|
518
|
+
private static Bitmap loadBitmapFromBase64(Uri imageUri) {
|
|
519
|
+
Bitmap sourceImage = null;
|
|
520
|
+
String imagePath = imageUri.getSchemeSpecificPart();
|
|
521
|
+
int commaLocation = imagePath.indexOf(',');
|
|
522
|
+
if (commaLocation != -1) {
|
|
523
|
+
final String mimeType = imagePath.substring(0, commaLocation).replace('\\','/').toLowerCase();
|
|
524
|
+
final boolean isJpeg = mimeType.startsWith(IMAGE_JPEG);
|
|
525
|
+
final boolean isPng = !isJpeg && mimeType.startsWith(IMAGE_PNG);
|
|
526
|
+
|
|
527
|
+
if (isJpeg || isPng) {
|
|
528
|
+
// base64 image. Convert to a bitmap.
|
|
529
|
+
final String encodedImage = imagePath.substring(commaLocation + 1);
|
|
530
|
+
final byte[] decodedString = Base64.decode(encodedImage, Base64.DEFAULT);
|
|
531
|
+
sourceImage = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return sourceImage;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Create a resized version of the given image and returns a Bitmap object
|
|
540
|
+
* ready to be saved or converted. Ensure that the result is cleaned up after use
|
|
541
|
+
* by using recycle
|
|
542
|
+
*/
|
|
543
|
+
public static Bitmap createResizedImage(Context context, Uri imageUri, int newWidth,
|
|
544
|
+
int newHeight, int quality, int rotation,
|
|
545
|
+
String mode, boolean onlyScaleDown) throws IOException {
|
|
546
|
+
Bitmap sourceImage = null;
|
|
547
|
+
String imageUriScheme = imageUri.getScheme();
|
|
548
|
+
|
|
549
|
+
if (imageUriScheme == null ||
|
|
550
|
+
imageUriScheme.equalsIgnoreCase(SCHEME_FILE) ||
|
|
551
|
+
imageUriScheme.equalsIgnoreCase(SCHEME_CONTENT)
|
|
552
|
+
) {
|
|
553
|
+
sourceImage = StreamChatReactNative.loadBitmapFromFile(context, imageUri, newWidth, newHeight);
|
|
554
|
+
} else if (imageUriScheme.equalsIgnoreCase(SCHEME_HTTP) || imageUriScheme.equalsIgnoreCase(SCHEME_HTTPS)){
|
|
555
|
+
sourceImage = StreamChatReactNative.loadBitmapFromURL(imageUri, newWidth, newHeight);
|
|
556
|
+
} else if (imageUriScheme.equalsIgnoreCase(SCHEME_DATA)) {
|
|
557
|
+
sourceImage = StreamChatReactNative.loadBitmapFromBase64(imageUri);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (sourceImage == null) {
|
|
561
|
+
throw new IOException("Unable to load source image from path");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
// Rotate if necessary. Rotate first because we will otherwise
|
|
566
|
+
// get wrong dimensions if we want the new dimensions to be after rotation.
|
|
567
|
+
// NOTE: This will "fix" the image using it's exif info if it is rotated as well.
|
|
568
|
+
Bitmap rotatedImage = sourceImage;
|
|
569
|
+
Matrix matrix = getOrientationMatrix(context, imageUri);
|
|
570
|
+
rotatedImage = StreamChatReactNative.rotateImage(sourceImage, matrix, rotation);
|
|
571
|
+
|
|
572
|
+
if(rotatedImage == null){
|
|
573
|
+
throw new IOException("Unable to rotate image. Most likely due to not enough memory.");
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (rotatedImage != sourceImage) {
|
|
577
|
+
sourceImage.recycle();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Scale image
|
|
581
|
+
Bitmap scaledImage = StreamChatReactNative.resizeImage(rotatedImage, newWidth, newHeight, mode, onlyScaleDown);
|
|
582
|
+
|
|
583
|
+
if(scaledImage == null){
|
|
584
|
+
throw new IOException("Unable to resize image. Most likely due to not enough memory.");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (scaledImage != rotatedImage) {
|
|
588
|
+
rotatedImage.recycle();
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return scaledImage;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
package com.streamchatreactnative;
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint;
|
|
4
|
+
import android.graphics.Bitmap;
|
|
5
|
+
import android.net.Uri;
|
|
6
|
+
import android.os.AsyncTask;
|
|
7
|
+
import android.util.Log;
|
|
8
|
+
|
|
9
|
+
import androidx.annotation.Nullable;
|
|
10
|
+
import androidx.annotation.NonNull;
|
|
11
|
+
|
|
12
|
+
import com.facebook.react.bridge.Arguments;
|
|
13
|
+
import com.facebook.react.bridge.GuardedAsyncTask;
|
|
14
|
+
import com.facebook.react.bridge.Promise;
|
|
15
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
16
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
17
|
+
import com.facebook.react.bridge.WritableMap;
|
|
18
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
import java.io.File;
|
|
22
|
+
import java.io.IOException;
|
|
23
|
+
import java.util.UUID;
|
|
24
|
+
|
|
25
|
+
public class StreamChatReactNativeModule extends StreamChatReactNativeSpec {
|
|
26
|
+
public static final String NAME = "StreamChatReactNative";
|
|
27
|
+
|
|
28
|
+
StreamChatReactNativeModule(ReactApplicationContext reactContext) {
|
|
29
|
+
super(reactContext);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Override
|
|
33
|
+
@NonNull
|
|
34
|
+
public String getName() {
|
|
35
|
+
return NAME;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@ReactMethod
|
|
39
|
+
public void createResizedImage(String uri, double width, double height, String format, double quality, String mode, boolean onlyScaleDown, Double rotation, @Nullable String outputPath, Boolean keepMeta, Promise promise) {
|
|
40
|
+
WritableMap options = Arguments.createMap();
|
|
41
|
+
options.putString("mode", mode);
|
|
42
|
+
options.putBoolean("onlyScaleDown", onlyScaleDown);
|
|
43
|
+
|
|
44
|
+
// Run in guarded async task to prevent blocking the React bridge
|
|
45
|
+
new GuardedAsyncTask<Void, Void>(this.getReactApplicationContext()) {
|
|
46
|
+
@Override
|
|
47
|
+
protected void doInBackgroundGuarded(Void... params) {
|
|
48
|
+
try {
|
|
49
|
+
Object response = createResizedImageWithExceptions(uri, (int) width, (int) height, format, (int) quality, rotation.intValue(), outputPath, keepMeta, options);
|
|
50
|
+
promise.resolve(response);
|
|
51
|
+
}
|
|
52
|
+
catch (IOException e) {
|
|
53
|
+
promise.reject(e);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@SuppressLint("LongLogTag")
|
|
60
|
+
private Object createResizedImageWithExceptions(String imagePath, int newWidth, int newHeight,
|
|
61
|
+
String compressFormatString, int quality, int rotation, String outputPath,
|
|
62
|
+
final boolean keepMeta,
|
|
63
|
+
final ReadableMap options) throws IOException {
|
|
64
|
+
|
|
65
|
+
Bitmap.CompressFormat compressFormat = Bitmap.CompressFormat.valueOf(compressFormatString);
|
|
66
|
+
Uri imageUri = Uri.parse(imagePath);
|
|
67
|
+
|
|
68
|
+
Bitmap scaledImage = StreamChatReactNative.createResizedImage(this.getReactApplicationContext(), imageUri, newWidth, newHeight, quality, rotation,
|
|
69
|
+
options.getString("mode"), options.getBoolean("onlyScaleDown"));
|
|
70
|
+
|
|
71
|
+
if (scaledImage == null) {
|
|
72
|
+
throw new IOException("The image failed to be resized; invalid Bitmap result.");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Save the resulting image
|
|
76
|
+
File path = this.getReactApplicationContext().getCacheDir();
|
|
77
|
+
if (outputPath != null) {
|
|
78
|
+
path = new File(outputPath);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
File resizedImage = StreamChatReactNative.saveImage(scaledImage, path, UUID.randomUUID().toString(), compressFormat, quality);
|
|
82
|
+
WritableMap response = Arguments.createMap();
|
|
83
|
+
|
|
84
|
+
// If resizedImagePath is empty and this wasn't caught earlier, throw.
|
|
85
|
+
if (resizedImage.isFile()) {
|
|
86
|
+
response.putString("path", resizedImage.getAbsolutePath());
|
|
87
|
+
response.putString("uri", Uri.fromFile(resizedImage).toString());
|
|
88
|
+
response.putString("name", resizedImage.getName());
|
|
89
|
+
response.putDouble("size", resizedImage.length());
|
|
90
|
+
response.putDouble("width", scaledImage.getWidth());
|
|
91
|
+
response.putDouble("height", scaledImage.getHeight());
|
|
92
|
+
|
|
93
|
+
// Copy file's metadata/exif info if required
|
|
94
|
+
if(keepMeta){
|
|
95
|
+
try{
|
|
96
|
+
StreamChatReactNative.copyExif(this.getReactApplicationContext(), imageUri, resizedImage.getAbsolutePath());
|
|
97
|
+
}
|
|
98
|
+
catch(Exception ignored){
|
|
99
|
+
Log.e("StreamChatReactNative::createResizedImageWithExceptions", "EXIF copy failed", ignored);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
throw new IOException("Error getting resized image path");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
// Clean up bitmap
|
|
108
|
+
scaledImage.recycle();
|
|
109
|
+
return response;
|
|
110
|
+
}
|
|
111
|
+
}
|