feat(android): add 'message' field to media error CB-11641 (#357)

* CB-11641 android: add 'message' field to media error

It was not possible before to get error message.This updated Android implementation to return error code with message similar to iOS.

* style: apply lint corrections
* refactor(android): private API sendStatusChange

Co-authored-by: Nikita Matrosov <matrosov.nikita94@gmail.com>
diff --git a/src/android/AudioPlayer.java b/src/android/AudioPlayer.java
index 079356b..e19689f 100644
--- a/src/android/AudioPlayer.java
+++ b/src/android/AudioPlayer.java
@@ -158,10 +158,11 @@
      * @param file              The name of the file
      */
     public void startRecording(String file) {
+        String errorMessage;
         switch (this.mode) {
         case PLAY:
-            LOG.d(LOG_TAG, "AudioPlayer Error: Can't record in play mode.");
-            sendErrorStatus(MEDIA_ERR_ABORTED);
+            errorMessage = "AudioPlayer Error: Can't record in play mode.";
+            sendErrorStatus(MEDIA_ERR_ABORTED, errorMessage);
             break;
         case NONE:
             this.audioFile = file;
@@ -182,11 +183,11 @@
                 e.printStackTrace();
             }
 
-            sendErrorStatus(MEDIA_ERR_ABORTED);
+            sendErrorStatus(MEDIA_ERR_ABORTED, null);
             break;
         case RECORD:
-            LOG.d(LOG_TAG, "AudioPlayer Error: Already recording.");
-            sendErrorStatus(MEDIA_ERR_ABORTED);
+            errorMessage = "AudioPlayer Error: Already recording.";
+            sendErrorStatus(MEDIA_ERR_ABORTED, errorMessage);
         }
     }
 
@@ -365,7 +366,7 @@
                 this.player.seekTo(milliseconds);
             }
             LOG.d(LOG_TAG, "Send a onStatus update for the new seek");
-            sendStatusChange(MEDIA_POSITION, null, (milliseconds / 1000.0f));
+            sendStatusChange(MEDIA_POSITION, null, (milliseconds / 1000.0f), null);
         }
         else {
             this.seekOnPrepared = milliseconds;
@@ -383,8 +384,8 @@
             this.setState(STATE.MEDIA_PAUSED);
         }
         else {
-            LOG.d(LOG_TAG, "AudioPlayer Error: pausePlaying() called during invalid state: " + this.state.ordinal());
-            sendErrorStatus(MEDIA_ERR_NONE_ACTIVE);
+            String errorMessage = "AudioPlayer Error: pausePlaying() called during invalid state: " + this.state.ordinal();
+            sendErrorStatus(MEDIA_ERR_NONE_ACTIVE, errorMessage);
         }
     }
 
@@ -399,8 +400,8 @@
             this.setState(STATE.MEDIA_STOPPED);
         }
         else {
-            LOG.d(LOG_TAG, "AudioPlayer Error: stopPlaying() called during invalid state: " + this.state.ordinal());
-            sendErrorStatus(MEDIA_ERR_NONE_ACTIVE);
+            String errorMessage = "AudioPlayer Error: stopPlaying() called during invalid state: " + this.state.ordinal();
+            sendErrorStatus(MEDIA_ERR_NONE_ACTIVE, errorMessage);
         }
     }
 
@@ -429,7 +430,7 @@
     public long getCurrentPosition() {
         if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) {
             int curPos = this.player.getCurrentPosition();
-            sendStatusChange(MEDIA_POSITION, null, (curPos / 1000.0f));
+            sendStatusChange(MEDIA_POSITION, null, (curPos / 1000.0f), null);
             return curPos;
         }
         else {
@@ -511,7 +512,7 @@
         this.prepareOnly = true;
 
         // Send status notification to JavaScript
-        sendStatusChange(MEDIA_DURATION, null, this.duration);
+        sendStatusChange(MEDIA_DURATION, null, this.duration, null);
     }
 
     /**
@@ -532,14 +533,14 @@
      * @param arg2              an extra code, specific to the error.
      */
     public boolean onError(MediaPlayer player, int arg1, int arg2) {
-        LOG.d(LOG_TAG, "AudioPlayer.onError(" + arg1 + ", " + arg2 + ")");
+        String errorMessage = "AudioPlayer.onError(" + arg1 + ", " + arg2 + ")";
 
         // we don't want to send success callback
         // so we don't call setState() here
         this.state = STATE.MEDIA_STOPPED;
         this.destroy();
         // Send error notification to JavaScript
-        sendErrorStatus(arg1);
+        sendErrorStatus(arg1, errorMessage);
 
         return false;
     }
@@ -551,7 +552,7 @@
      */
     private void setState(STATE state) {
         if (this.state != state) {
-            sendStatusChange(MEDIA_STATE, null, (float)state.ordinal());
+            sendStatusChange(MEDIA_STATE, null, (float)state.ordinal(), null);
         }
         this.state = state;
     }
@@ -587,8 +588,8 @@
         if (this.player != null) {
             this.player.setVolume(volume, volume);
         } else {
-            LOG.d(LOG_TAG, "AudioPlayer Error: Cannot set volume until the audio file is initialized.");
-            sendErrorStatus(MEDIA_ERR_NONE_ACTIVE);
+            String errorMessage = "AudioPlayer Error: Cannot set volume until the audio file is initialized.";
+            sendErrorStatus(MEDIA_ERR_NONE_ACTIVE, errorMessage);
         }
     }
 
@@ -604,8 +605,8 @@
         case PLAY:
             break;
         case RECORD:
-            LOG.d(LOG_TAG, "AudioPlayer Error: Can't play in record mode.");
-            sendErrorStatus(MEDIA_ERR_ABORTED);
+            String errorMessage = "AudioPlayer Error: Can't play in record mode.";
+            sendErrorStatus(MEDIA_ERR_ABORTED, errorMessage);
             return false; //player is not ready
         }
         return true;
@@ -627,7 +628,7 @@
                     try {
                         this.loadAudioFile(file);
                     } catch (Exception e) {
-                        sendErrorStatus(MEDIA_ERR_ABORTED);
+                        sendErrorStatus(MEDIA_ERR_ABORTED, e.getMessage());
                     }
                     return false;
                 case MEDIA_LOADING:
@@ -651,7 +652,7 @@
                             try {
                                 this.loadAudioFile(file);
                             } catch (Exception e) {
-                                sendErrorStatus(MEDIA_ERR_ABORTED);
+                                sendErrorStatus(MEDIA_ERR_ABORTED, e.getMessage());
                             }
                             return false;//we´re not ready yet
                         }
@@ -667,14 +668,14 @@
                         try {
                             this.loadAudioFile(file);
                         } catch (Exception e) {
-                            sendErrorStatus(MEDIA_ERR_ABORTED);
+                            sendErrorStatus(MEDIA_ERR_ABORTED, e.getMessage());
                         }
                         //if we had to prepare the file, we won't be in the correct state for playback
                         return false;
                     }
                 default:
-                    LOG.d(LOG_TAG, "AudioPlayer Error: startPlaying() called during invalid state: " + this.state);
-                    sendErrorStatus(MEDIA_ERR_ABORTED);
+                    String errorMessage = "AudioPlayer Error: startPlaying() called during invalid state: " + this.state;
+                    sendErrorStatus(MEDIA_ERR_ABORTED, errorMessage);
             }
         }
         return false;
@@ -723,16 +724,19 @@
             }
     }
 
-    private void sendErrorStatus(int errorCode) {
-        sendStatusChange(MEDIA_ERROR, errorCode, null);
+    private void sendErrorStatus(int errorCode, String errorMessage) {
+        sendStatusChange(MEDIA_ERROR, errorCode, null, errorMessage);
     }
 
-    private void sendStatusChange(int messageType, Integer additionalCode, Float value) {
-
+    private void sendStatusChange(int messageType, Integer additionalCode, Float value, String errorMessage) {
         if (additionalCode != null && value != null) {
             throw new IllegalArgumentException("Only one of additionalCode or value can be specified, not both");
         }
 
+        if (errorMessage != null) {
+            LOG.d(LOG_TAG, errorMessage);
+        }
+
         JSONObject statusDetails = new JSONObject();
         try {
             statusDetails.put("id", this.id);
@@ -740,6 +744,11 @@
             if (additionalCode != null) {
                 JSONObject code = new JSONObject();
                 code.put("code", additionalCode.intValue());
+
+                if (errorMessage != null) {
+                    code.put("message", errorMessage);
+                }
+
                 statusDetails.put("value", code);
             }
             else if (value != null) {
diff --git a/tests/tests.js b/tests/tests.js
index 6b9c389..b26cc42 100644
--- a/tests/tests.js
+++ b/tests/tests.js
@@ -232,6 +232,23 @@
             badMedia.play();
         });
 
+        it("media.spec.19 MediaError instance should contain 'code' and 'message' fields", function (done) {
+            var context = this;
+            var fileName = 'invalid.file.name';
+            var badMedia = new Media(fileName, succeed.bind(null, done, ' badMedia = new Media , Unexpected succees callback, it should not create Media object with invalid file name'), function (result) {
+                if (context.done) return;
+                context.done = true;
+                expect(result).toBeDefined();
+                expect(result.code).toBeDefined();
+                expect(result.message).toBeDefined();
+                if (badMedia) {
+                    badMedia.release();
+                }
+                done();
+            });
+            badMedia.play();
+        });
+
         describe('actual playback', function () {
             var checkInterval, media;