react-native-video-trim 6.2.0 → 6.2.2

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.
@@ -8,6 +8,7 @@ let FILE_PREFIX = "trimmedVideo"
8
8
  public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDelegate {
9
9
  // MARK: instance private props
10
10
  private var isShowing = false
11
+ private var isTrimming = false
11
12
  private var vc: VideoTrimmerViewController?
12
13
  private var outputFile: URL?
13
14
  private var editorConfig: NSDictionary?
@@ -260,6 +261,9 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
260
261
  }
261
262
 
262
263
  private func trim(viewController: VideoTrimmerViewController, inputFile: URL, videoDuration: Double, startTime: Double, endTime: Double, isVideoType: Bool) {
264
+ guard !isTrimming else { return }
265
+ isTrimming = true
266
+
263
267
  vc?.pausePlayer()
264
268
 
265
269
  let timestamp = Int(Date().timeIntervalSince1970)
@@ -297,7 +301,9 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
297
301
  self.emitEventToJS("onCancelTrimming", eventData: nil)
298
302
  }
299
303
 
300
- progressAlert.dismiss(animated: true)
304
+ progressAlert.dismiss(animated: true) {
305
+ self.isTrimming = false
306
+ }
301
307
  })
302
308
 
303
309
  // Create Cancel button with action handlder
@@ -318,7 +324,9 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
318
324
  self.emitEventToJS("onCancelTrimming", eventData: nil)
319
325
  }
320
326
 
321
- progressAlert.dismiss(animated: true)
327
+ progressAlert.dismiss(animated: true) {
328
+ self.isTrimming = false
329
+ }
322
330
  }
323
331
 
324
332
  }
@@ -358,14 +366,11 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
358
366
 
359
367
  ffmpegSession = FFmpegKit.execute(withArgumentsAsync: cmds, withCompleteCallback: { session in
360
368
 
361
- // always hide progressAlert
362
- DispatchQueue.main.async {
363
- progressAlert.dismiss(animated: true)
364
- }
365
-
366
369
  let state = session?.getState()
367
370
  let returnCode = session?.getReturnCode()
368
371
 
372
+ var shouldCloseEditor = false
373
+
369
374
  if ReturnCode.isSuccess(returnCode) {
370
375
  let eventPayload: [String: Any] = ["outputPath": self.outputFile!.absoluteString, "startTime": (startTime * 1000).rounded(), "endTime": (endTime * 1000).rounded(), "duration": (videoDuration * 1000).rounded()]
371
376
  self.emitEventToJS("onFinishTrimming", eventData: eventPayload)
@@ -396,20 +401,24 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
396
401
  }
397
402
  }
398
403
  } else if self.openDocumentsOnFinish {
399
- self.saveFileToFilesApp(fileURL: self.outputFile!)
400
-
401
- // must return otherwise editor will close
404
+ DispatchQueue.main.async {
405
+ progressAlert.dismiss(animated: true) {
406
+ self.isTrimming = false
407
+ self.saveFileToFilesApp(fileURL: self.outputFile!)
408
+ }
409
+ }
402
410
  return
403
411
  } else if self.openShareSheetOnFinish {
404
- self.shareFile(fileURL: self.outputFile!)
405
-
406
- // must return otherwise editor will close
412
+ DispatchQueue.main.async {
413
+ progressAlert.dismiss(animated: true) {
414
+ self.isTrimming = false
415
+ self.shareFile(fileURL: self.outputFile!)
416
+ }
417
+ }
407
418
  return
408
419
  }
409
420
 
410
- if self.closeWhenFinish {
411
- self.closeEditor(delay: 500)
412
- }
421
+ shouldCloseEditor = self.closeWhenFinish
413
422
 
414
423
  } else if ReturnCode.isCancel(returnCode) {
415
424
  // CANCEL
@@ -417,8 +426,15 @@ public class VideoTrim: RCTEventEmitter, AssetLoaderDelegate, UIDocumentPickerDe
417
426
  } else {
418
427
  // FAILURE
419
428
  self.onError(message: "Command failed with state \(String(describing: FFmpegKitConfig.sessionState(toString: state ?? .failed))) and rc \(String(describing: returnCode)).\(String(describing: session?.getFailStackTrace()))", code: .trimmingFailed)
420
- if self.closeWhenFinish {
421
- self.closeEditor(delay: 500)
429
+ shouldCloseEditor = self.closeWhenFinish
430
+ }
431
+
432
+ DispatchQueue.main.async {
433
+ progressAlert.dismiss(animated: true) {
434
+ self.isTrimming = false
435
+ if shouldCloseEditor {
436
+ self.closeEditor()
437
+ }
422
438
  }
423
439
  }
424
440
 
@@ -809,15 +825,18 @@ extension VideoTrim {
809
825
  @objc(closeEditor:)
810
826
  public func closeEditor(delay: Int = 0) {
811
827
  guard let vc = vc else { return }
812
- // some how in case we trim a very short video the view controller is still visible after first .dismiss call
813
- // even the file is successfully saved
814
- // that's why we need a small delay here to ensure vc will be dismissed
815
- DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(delay)) {
828
+ let dismissBlock = {
816
829
  vc.dismiss(animated: true, completion: {
817
830
  self.emitEventToJS("onHide", eventData: nil)
818
831
  self.isShowing = false
819
832
  })
820
833
  }
834
+
835
+ if delay > 0 {
836
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(delay), execute: dismissBlock)
837
+ } else {
838
+ DispatchQueue.main.async(execute: dismissBlock)
839
+ }
821
840
  }
822
841
 
823
842
  // Old Arch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-video-trim",
3
- "version": "6.2.0",
3
+ "version": "6.2.2",
4
4
  "description": "Video trimmer for your React Native app",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -1,5 +0,0 @@
1
- package com.videotrim.interfaces;
2
-
3
- public interface IVideoTrimmerView {
4
- void onDestroy();
5
- }
@@ -1,19 +0,0 @@
1
- package com.videotrim.interfaces;
2
-
3
- import com.facebook.react.bridge.ReadableMap;
4
- import com.facebook.react.bridge.WritableMap;
5
- import com.videotrim.enums.ErrorCode;
6
-
7
- import java.util.Map;
8
-
9
- public interface VideoTrimListener {
10
- void onLoad(int duration);
11
- void onTrimmingProgress(int percentage);
12
- void onFinishTrim(String url, long startMs, long endMs, int videoDuration);
13
- void onCancelTrim();
14
- void onError(String errorMessage, ErrorCode errorCode);
15
- void onCancel();
16
- void onSave();
17
- void onLog(WritableMap log);
18
- void onStatistics(WritableMap statistics);
19
- }
@@ -1,92 +0,0 @@
1
- package com.videotrim.utils;
2
-
3
- import android.media.MediaMetadataRetriever;
4
- import android.net.Uri;
5
- import android.util.Log;
6
-
7
- import java.io.File;
8
- import java.io.IOException;
9
- import java.util.HashMap;
10
-
11
- public class MediaMetadataUtil {
12
-
13
- private static final String TAG = "MediaMetadataUtil";
14
-
15
- // Function to return MediaMetadataRetriever or null
16
- public static MediaMetadataRetriever getMediaMetadataRetriever(String source) {
17
- MediaMetadataRetriever retriever = new MediaMetadataRetriever();
18
- try {
19
- if (source.startsWith("http://") || source.startsWith("https://")) {
20
- retriever.setDataSource(source, new HashMap<>());
21
- } else {
22
- String filePath = source;
23
-
24
- // if "source" is not a valid file path, try to parse it as a URI
25
- if (!StorageUtil.isFileExists(filePath)) {
26
- Log.e(TAG, "File does not exist, trying to parse as URI: " + source);
27
-
28
- Uri uri = Uri.parse(source);
29
- filePath = uri.getPath();
30
-
31
- if (!StorageUtil.isFileExists(filePath)) {
32
- Log.e(TAG, "File does not exist at path: " + filePath);
33
- return null;
34
- }
35
- }
36
-
37
- retriever.setDataSource(filePath);
38
- }
39
- return retriever;
40
- } catch (Exception e) {
41
- Log.e(TAG, "Error setting data source", e);
42
- try {
43
- retriever.release();
44
- } catch (Exception ee) {
45
- Log.e(TAG, "Error releasing retriever", ee);
46
- }
47
- return null;
48
- }
49
- }
50
-
51
- public static void checkFileValidity(String urlString, FileValidityCallback callback) {
52
- new Thread(() -> {
53
- boolean isValid = false;
54
- String fileType = "unknown";
55
- long duration;
56
- MediaMetadataRetriever retriever = getMediaMetadataRetriever(urlString);
57
- if (retriever == null) {
58
- callback.onResult(false, fileType, -1L);
59
- return;
60
- }
61
-
62
- // Retrieve the duration
63
- String durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
64
- duration = durationStr == null ? -1L : Long.parseLong(durationStr);
65
-
66
- // Determine the type
67
- String hasVideo = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
68
- if (hasVideo != null && hasVideo.equals("yes")) {
69
- fileType = "video";
70
- isValid = true;
71
- } else {
72
- String hasAudio = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO);
73
- if (hasAudio != null && hasAudio.equals("yes")) {
74
- fileType = "audio";
75
- isValid = true;
76
- }
77
- }
78
-
79
- try {
80
- retriever.release();
81
- } catch (IOException e) {
82
- Log.e(TAG, "Error releasing retriever", e);
83
- }
84
- callback.onResult(isValid, fileType, isValid ? duration : -1L);
85
- }).start();
86
- }
87
-
88
- public interface FileValidityCallback {
89
- void onResult(boolean isValid, String fileType, Long duration);
90
- }
91
- }
92
-
@@ -1,147 +0,0 @@
1
- package com.videotrim.utils;
2
-
3
- import android.content.ContentValues;
4
- import android.content.Context;
5
- import android.media.MediaScannerConnection;
6
- import android.net.Uri;
7
- import android.os.Build;
8
- import android.os.Environment;
9
- import android.provider.MediaStore;
10
- import android.text.TextUtils;
11
-
12
- import com.facebook.react.bridge.ReactApplicationContext;
13
-
14
- import java.io.File;
15
- import java.io.FileInputStream;
16
- import java.io.FileOutputStream;
17
- import java.io.IOException;
18
- import java.io.InputStream;
19
- import java.io.OutputStream;
20
- import java.util.ArrayList;
21
- import java.util.List;
22
-
23
- public class StorageUtil {
24
- public static String getOutputPath(Context context, String mOutputExt) {
25
- long timestamp = System.currentTimeMillis() / 1000;
26
- File file = new File(context.getFilesDir(), VideoTrimmerUtil.FILE_PREFIX + "_" + timestamp + "." + mOutputExt);
27
- return file.getAbsolutePath();
28
- }
29
-
30
- public static boolean isFileExists(String filePath) {
31
- if (TextUtils.isEmpty(filePath)) return false;
32
- File file = new File(filePath);
33
- return file.exists();
34
- }
35
-
36
- public static String[] listFiles(Context context) {
37
- File filesDir = context.getFilesDir();
38
- File[] files = filesDir.listFiles((dir, name) -> name.startsWith(VideoTrimmerUtil.FILE_PREFIX));
39
-
40
- List<String> fileUrls = new ArrayList<>();
41
- if (files != null) {
42
- for (File file : files) {
43
- fileUrls.add(file.getAbsolutePath());
44
- }
45
- }
46
-
47
- return fileUrls.toArray(new String[0]);
48
- }
49
-
50
- public static boolean deleteFile(String path) {
51
- if (TextUtils.isEmpty(path)) return true;
52
- return deleteFile(new File(path));
53
- }
54
-
55
- public static boolean deleteFile(File file) {
56
- if (file == null || !file.exists()) return true;
57
-
58
- if (file.isFile()) {
59
- return file.delete();
60
- }
61
-
62
- if (!file.isDirectory()) {
63
- return false;
64
- }
65
-
66
- for (File f : file.listFiles()) {
67
- if (f.isFile()) {
68
- f.delete();
69
- } else if (f.isDirectory()) {
70
- deleteFile(f);
71
- }
72
- }
73
- return file.delete();
74
- }
75
-
76
- public static void saveVideoToGallery(ReactApplicationContext context, String videoFilePath) throws IOException {
77
- File videoFile = new File(videoFilePath);
78
-
79
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
80
- // For Android 10 and higher (API >= 29)
81
- saveVideoUsingMediaStore(context, videoFile);
82
- } else {
83
- // For Android 9 and below (API < 29)
84
- try {
85
- saveVideoUsingTraditionalStorage(context, videoFile);
86
- } catch (IOException e) {
87
- throw new RuntimeException(e);
88
- }
89
- }
90
- }
91
-
92
- // Save video using MediaStore for API level >= 29
93
- private static void saveVideoUsingMediaStore(Context context, File videoFile) {
94
- ContentValues values = new ContentValues();
95
- values.put(MediaStore.Video.Media.TITLE, "My Video Title");
96
- values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
97
- values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM);
98
- Uri uri = context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
99
-
100
- if (uri != null) {
101
- try {
102
- OutputStream outputStream = context.getContentResolver().openOutputStream(uri);
103
- copyFile(videoFile, outputStream);
104
- MediaScannerConnection.scanFile(context, new String[]{uri.toString()}, new String[]{"video/*"}, null);
105
- } catch (IOException e) {
106
- e.printStackTrace();
107
- }
108
- }
109
- }
110
-
111
- // Save video using traditional storage for API level < 29
112
- private static void saveVideoUsingTraditionalStorage(Context context, File videoFile) throws IOException {
113
- File galleryDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
114
- File destinationFile = new File(galleryDirectory, videoFile.getName());
115
- copyFile(videoFile, destinationFile);
116
- MediaScannerConnection.scanFile(context, new String[]{destinationFile.getAbsolutePath()}, new String[]{"video/*"}, null);
117
- }
118
-
119
- private static void copyFile(File sourceFile, OutputStream outputStream) throws IOException {
120
- InputStream inputStream = new FileInputStream(sourceFile);
121
- copyFile(inputStream, outputStream);
122
- }
123
-
124
- private static void copyFile(File sourceFile, File destFile) throws IOException {
125
- InputStream inputStream = new FileInputStream(sourceFile);
126
- OutputStream outputStream = new FileOutputStream(destFile);
127
- copyFile(inputStream, outputStream);
128
- }
129
-
130
- private static void copyFile(InputStream inputStream, OutputStream outputStream) throws IOException {
131
- try {
132
- byte[] buffer = new byte[1024];
133
- int length;
134
- while ((length = inputStream.read(buffer)) > 0) {
135
- outputStream.write(buffer, 0, length);
136
- }
137
-
138
- } finally {
139
- if (inputStream != null) {
140
- inputStream.close();
141
- }
142
- if (outputStream != null) {
143
- outputStream.close();
144
- }
145
- }
146
- }
147
- }
@@ -1,171 +0,0 @@
1
- package com.videotrim.utils;
2
-
3
- import android.annotation.SuppressLint;
4
- import android.graphics.Bitmap;
5
- import android.media.MediaMetadataRetriever;
6
- import android.util.Log;
7
-
8
- import com.arthenica.ffmpegkit.FFmpegKit;
9
- import com.arthenica.ffmpegkit.FFmpegSession;
10
- import com.arthenica.ffmpegkit.ReturnCode;
11
- import com.arthenica.ffmpegkit.SessionState;
12
- import com.facebook.react.bridge.Arguments;
13
- import com.facebook.react.bridge.WritableMap;
14
- import com.videotrim.enums.ErrorCode;
15
- import com.videotrim.interfaces.VideoTrimListener;
16
-
17
- import java.text.SimpleDateFormat;
18
- import java.util.ArrayList;
19
- import java.util.Date;
20
- import java.util.List;
21
- import java.util.TimeZone;
22
-
23
- import iknow.android.utils.DeviceUtil;
24
- import iknow.android.utils.UnitConverter;
25
- import iknow.android.utils.callback.SingleCallback;
26
- import iknow.android.utils.thread.BackgroundExecutor;
27
-
28
-
29
- public class VideoTrimmerUtil {
30
-
31
- private static final String TAG = VideoTrimmerUtil.class.getSimpleName();
32
- public static final String FILE_PREFIX = "trimmedVideo";
33
- public static final long MIN_SHOOT_DURATION = 1000L;// min 3 seconds for trimming
34
- public static final int VIDEO_MAX_TIME = 10;// max 10 seconds for trimming
35
- public static final long MAX_SHOOT_DURATION = VIDEO_MAX_TIME * 1000L;
36
- // public static long maxShootDuration = 10 * 1000L;
37
- public static int MAX_COUNT_RANGE = 10; // how many images in the highlight range of seek bar
38
- public static int SCREEN_WIDTH_FULL = DeviceUtil.getDeviceWidth();
39
- public static final int RECYCLER_VIEW_PADDING = UnitConverter.dpToPx(35);
40
- public static String DEFAULT_AUDIO_EXTENSION = ".wav";
41
- public static int VIDEO_FRAMES_WIDTH = SCREEN_WIDTH_FULL - RECYCLER_VIEW_PADDING * 2;
42
- // public static final int THUMB_WIDTH = (SCREEN_WIDTH_FULL - RECYCLER_VIEW_PADDING * 2) / VIDEO_MAX_TIME;
43
- public static int mThumbWidth = 0; // make it automatic
44
- public static final int THUMB_HEIGHT = UnitConverter.dpToPx(50); // x2 for better resolution
45
- public static final int THUMB_WIDTH = UnitConverter.dpToPx(25); // x2 for better resolution
46
- private static final int THUMB_RESOLUTION_RES = 2; // double thumb resolution for better quality
47
-
48
- public static FFmpegSession trim(String inputFile, String outputFile, int videoDuration, long startMs, long endMs, boolean enableRotation, double rotationAngle, final VideoTrimListener callback) {
49
- // Get the current date and time
50
- Date currentDate = new Date();
51
-
52
- // Create a SimpleDateFormat object with the desired format
53
- @SuppressLint("SimpleDateFormat") SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
54
-
55
- // Set the timezone to UTC
56
- dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
57
- // Format the current date and time
58
- String formattedDateTime = dateFormat.format(currentDate);
59
-
60
- // create list to store commands
61
- List<String> cmds = new ArrayList<>();
62
- cmds.add("-ss");
63
- cmds.add(startMs + "ms");
64
- cmds.add("-to");
65
- cmds.add(endMs + "ms");
66
-
67
- if (enableRotation) {
68
- // add "-display_rotation" and rotation angle to the command, contact, not creating new
69
- cmds.add("-display_rotation");
70
- cmds.add(String.valueOf(rotationAngle));
71
- }
72
-
73
- cmds.add("-i");
74
- cmds.add(inputFile);
75
- cmds.add("-c");
76
- cmds.add("copy");
77
- cmds.add("-metadata");
78
- cmds.add("creation_time=" + formattedDateTime);
79
- cmds.add(outputFile);
80
-
81
- String[] command = cmds.toArray(new String[0]);
82
- String cmdStr = "Command: " + String.join(" ", command);
83
-
84
- Log.d(TAG, cmdStr);
85
-
86
- WritableMap m = Arguments.createMap();
87
- m.putString("message", cmdStr);
88
-
89
- callback.onLog(m);
90
-
91
- return FFmpegKit.executeWithArgumentsAsync(command, session -> {
92
- SessionState state = session.getState();
93
- ReturnCode returnCode = session.getReturnCode();
94
- if (ReturnCode.isSuccess(session.getReturnCode())) {
95
- // SUCCESS
96
- callback.onFinishTrim(outputFile, startMs, endMs, videoDuration);
97
- } else if (ReturnCode.isCancel(session.getReturnCode())) {
98
- // CANCEL
99
- callback.onCancelTrim();
100
- } else {
101
- // FAILURE
102
- String errorMessage = String.format("Command failed with state %s and rc %s.%s", state, returnCode, session.getFailStackTrace());
103
- callback.onError(errorMessage, ErrorCode.TRIMMING_FAILED);
104
- }
105
- }, log -> {
106
- Log.d(TAG, "FFmpeg process started with log " + log.getMessage());
107
-
108
- WritableMap map = Arguments.createMap();
109
- map.putInt("level", log.getLevel().getValue());
110
- map.putString("message", log.getMessage());
111
- map.putDouble("sessionId", log.getSessionId());
112
- map.putString("logStr", log.toString());
113
-
114
- callback.onLog(map);
115
- }, statistics -> {
116
- int timeInMilliseconds = (int) statistics.getTime();
117
- if (timeInMilliseconds > 0) {
118
- int completePercentage =
119
- (timeInMilliseconds * 100) / videoDuration;
120
- callback.onTrimmingProgress(Math.min(Math.max(completePercentage, 0), 100));
121
- }
122
-
123
- WritableMap map = Arguments.createMap();
124
- map.putDouble("sessionId", statistics.getSessionId());
125
- map.putInt("videoFrameNumber", statistics.getVideoFrameNumber());
126
- map.putDouble("videoFps", statistics.getVideoFps());
127
- map.putDouble("videoQuality", statistics.getVideoQuality());
128
- map.putDouble("size", statistics.getSize());
129
- map.putDouble("time", statistics.getTime());
130
- map.putDouble("bitrate", statistics.getBitrate());
131
- map.putDouble("speed", statistics.getSpeed());
132
- map.putString("statisticsStr", statistics.toString());
133
- callback.onStatistics(map);
134
- });
135
- }
136
-
137
- public static void shootVideoThumbInBackground(final MediaMetadataRetriever mediaMetadataRetriever, final int totalThumbsCount, final long startPosition,
138
- final long endPosition, final SingleCallback<Bitmap, Integer> callback) {
139
- BackgroundExecutor.execute(new BackgroundExecutor.Task("", 0L, "") {
140
- @Override public void execute() {
141
- try {
142
- // Retrieve media data use microsecond
143
- long interval = (endPosition - startPosition) / (totalThumbsCount - 1);
144
- for (long i = 0; i < totalThumbsCount; ++i) {
145
- long frameTime = startPosition + interval * i;
146
-
147
- Bitmap bitmap;
148
- try {
149
- bitmap = mediaMetadataRetriever.getFrameAtTime(frameTime * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
150
- } catch (final Throwable t) {
151
- // this can happen while thumbnails are being generated in background and we press Cancel
152
- t.printStackTrace();
153
- break;
154
- }
155
-
156
- if(bitmap == null) continue;
157
- try {
158
- bitmap = Bitmap.createScaledBitmap(bitmap, mThumbWidth * THUMB_RESOLUTION_RES, THUMB_HEIGHT * THUMB_RESOLUTION_RES, false);
159
- } catch (final Throwable t) {
160
- t.printStackTrace();
161
- }
162
- callback.onSingleCallback(bitmap, (int) interval);
163
- }
164
- } catch (final Throwable e) {
165
- Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
166
- }
167
- }
168
- });
169
- }
170
- }
171
-