@capgo/native-audio 8.4.0 → 8.4.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.
@@ -27,7 +27,9 @@ import static ee.forgr.audio.Constant.VOLUME;
27
27
  import android.Manifest;
28
28
  import android.app.NotificationChannel;
29
29
  import android.app.NotificationManager;
30
+ import android.app.PendingIntent;
30
31
  import android.content.Context;
32
+ import android.content.Intent;
31
33
  import android.content.res.AssetFileDescriptor;
32
34
  import android.content.res.AssetManager;
33
35
  import android.graphics.Bitmap;
@@ -38,6 +40,7 @@ import android.os.Build;
38
40
  import android.os.Handler;
39
41
  import android.os.Looper;
40
42
  import android.os.ParcelFileDescriptor;
43
+ import android.os.SystemClock;
41
44
  import android.support.v4.media.MediaMetadataCompat;
42
45
  import android.support.v4.media.session.MediaSessionCompat;
43
46
  import android.support.v4.media.session.PlaybackStateCompat;
@@ -93,6 +96,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
93
96
  private final Map<String, Handler> pendingPlayHandlers = new ConcurrentHashMap<>();
94
97
  private final Map<String, Runnable> pendingPlayRunnables = new ConcurrentHashMap<>();
95
98
  private final Map<String, JSObject> audioData = new ConcurrentHashMap<>();
99
+ private final Map<String, Integer> playbackStateByAssetId = new ConcurrentHashMap<>();
96
100
 
97
101
  // Notification center support
98
102
  private boolean showNotification = false;
@@ -102,6 +106,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
102
106
  private static final int NOTIFICATION_ID = 1001;
103
107
  private static final String CHANNEL_ID = "native_audio_channel";
104
108
  private static final int MAX_NOTIFICATION_ARTWORK_SIZE = 512;
109
+ private static final double NOTIFICATION_SKIP_SECONDS = 15.0;
105
110
 
106
111
  // Track playOnce assets for automatic cleanup
107
112
  private Set<String> playOnceAssets = new HashSet<>();
@@ -135,23 +140,34 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
135
140
  for (AudioAsset audio : audioAssetList.values()) {
136
141
  if (audio.isPlaying()) {
137
142
  audio.pause();
143
+ updateTrackedPlaybackState(audio.getAssetId(), PlaybackStateCompat.STATE_PAUSED);
138
144
  resumeList.add(audio);
139
145
  }
140
146
  }
147
+ syncCurrentPlaybackState("audioFocusLossTransient");
141
148
  } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
142
149
  // Resume playback
143
150
  if (resumeList != null) {
144
151
  while (!resumeList.isEmpty()) {
145
152
  AudioAsset audio = resumeList.remove(0);
146
153
  audio.resume();
154
+ updateTrackedPlaybackState(audio.getAssetId(), PlaybackStateCompat.STATE_PLAYING);
147
155
  }
148
156
  }
157
+ syncCurrentPlaybackState("audioFocusGain");
149
158
  } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
150
159
  // Stop playback - permanent loss
160
+ String stoppedAssetId = currentlyPlayingAssetId;
151
161
  for (AudioAsset audio : audioAssetList.values()) {
152
162
  audio.stop();
163
+ updateTrackedPlaybackState(audio.getAssetId(), PlaybackStateCompat.STATE_STOPPED);
153
164
  }
154
165
  audioManager.abandonAudioFocus(this);
166
+ if (isStringValid(stoppedAssetId)) {
167
+ clearNotification();
168
+ currentlyPlayingAssetId = null;
169
+ notifyPlaybackState(stoppedAssetId, "audioFocusLoss");
170
+ }
155
171
  }
156
172
  } catch (Exception ex) {
157
173
  Log.e(TAG, "Error handling audio focus change", ex);
@@ -177,11 +193,13 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
177
193
  boolean wasPlaying = audio.pause();
178
194
 
179
195
  if (wasPlaying) {
196
+ updateTrackedPlaybackState(entry.getKey(), PlaybackStateCompat.STATE_PAUSED);
180
197
  resumeList.add(audio);
181
198
  }
182
199
  }
183
200
  }
184
201
  }
202
+ syncCurrentPlaybackState("appPause");
185
203
  } catch (Exception ex) {
186
204
  Log.d(TAG, "Exception caught while listening for handleOnPause: " + ex.getLocalizedMessage());
187
205
  }
@@ -204,9 +222,11 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
204
222
 
205
223
  if (audio != null) {
206
224
  audio.resume();
225
+ updateTrackedPlaybackState(audio.getAssetId(), PlaybackStateCompat.STATE_PLAYING);
207
226
  }
208
227
  }
209
228
  }
229
+ syncCurrentPlaybackState("appResume");
210
230
  } catch (Exception ex) {
211
231
  Log.d(TAG, "Exception caught while listening for handleOnResume: " + ex.getLocalizedMessage());
212
232
  }
@@ -424,6 +444,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
424
444
  assetToUnload.unload();
425
445
  NativeAudio.this.audioAssetList.remove(assetId);
426
446
  }
447
+ NativeAudio.this.clearTrackedPlaybackState(assetId);
427
448
 
428
449
  // Remove from tracking sets
429
450
  NativeAudio.this.playOnceAssets.remove(assetId);
@@ -457,12 +478,14 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
457
478
  // Auto-play if requested
458
479
  if (autoPlay) {
459
480
  asset.play(0.0);
481
+ updateTrackedPlaybackState(assetId, PlaybackStateCompat.STATE_PLAYING);
460
482
 
461
483
  // Update notification if enabled
462
484
  if (showNotification) {
463
485
  currentlyPlayingAssetId = assetId;
464
486
  updateNotification(assetId);
465
487
  }
488
+ NativeAudio.this.notifyPlaybackState(assetId, "playOnce");
466
489
  }
467
490
 
468
491
  // Return the generated assetId
@@ -478,6 +501,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
478
501
  failedAsset.unload();
479
502
  NativeAudio.this.audioAssetList.remove(assetId);
480
503
  }
504
+ NativeAudio.this.clearTrackedPlaybackState(assetId);
481
505
  call.reject("Failed to load asset for playOnce: " + ex.getMessage());
482
506
  }
483
507
  } catch (Exception ex) {
@@ -602,10 +626,13 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
602
626
  handleFadeOut(asset, audioId, fadeOutDurationMs, fadeOutStartTimeSecs);
603
627
  }
604
628
 
629
+ updateTrackedPlaybackState(audioId, PlaybackStateCompat.STATE_PLAYING);
630
+
605
631
  if (showNotification) {
606
632
  currentlyPlayingAssetId = audioId;
607
633
  updateNotification(audioId);
608
634
  }
635
+ notifyPlaybackState(audioId, "play");
609
636
 
610
637
  call.resolve();
611
638
  } catch (Exception ex) {
@@ -717,12 +744,16 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
717
744
  resumeList.add(asset);
718
745
  }
719
746
 
747
+ updateTrackedPlaybackState(audioId, PlaybackStateCompat.STATE_PAUSED);
748
+
720
749
  // Update notification when paused
721
750
  if (showNotification) {
722
751
  updatePlaybackState(PlaybackStateCompat.STATE_PAUSED);
723
752
  updateNotification(audioId);
724
753
  }
725
754
 
755
+ notifyPlaybackState(audioId, "pause");
756
+
726
757
  call.resolve();
727
758
  } else {
728
759
  call.reject(ERROR_ASSET_NOT_LOADED + " - " + audioId);
@@ -761,13 +792,17 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
761
792
  data.remove("volumeBeforePause");
762
793
  setAudioAssetData(audioId, data);
763
794
  resumeList.add(asset);
795
+ updateTrackedPlaybackState(audioId, PlaybackStateCompat.STATE_PLAYING);
764
796
 
765
797
  // Update notification when resumed
766
798
  if (showNotification) {
799
+ currentlyPlayingAssetId = audioId;
767
800
  updatePlaybackState(PlaybackStateCompat.STATE_PLAYING);
768
801
  updateNotification(audioId);
769
802
  }
770
803
 
804
+ notifyPlaybackState(audioId, "resume");
805
+
771
806
  call.resolve();
772
807
  } else {
773
808
  call.reject(ERROR_ASSET_NOT_LOADED + " - " + audioId);
@@ -800,6 +835,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
800
835
  clearFadeOutToStopTimer(audioId);
801
836
  stopAudio(audioId, fadeOut, fadeOutDurationMs);
802
837
  audioData.remove(audioId);
838
+ updateTrackedPlaybackState(audioId, PlaybackStateCompat.STATE_STOPPED);
803
839
 
804
840
  // Clear notification when stopped
805
841
  if (showNotification) {
@@ -807,6 +843,8 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
807
843
  currentlyPlayingAssetId = null;
808
844
  }
809
845
 
846
+ notifyPlaybackState(audioId, "stop");
847
+
810
848
  call.resolve();
811
849
  } catch (Exception ex) {
812
850
  call.reject(ex.getMessage());
@@ -833,6 +871,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
833
871
  clearFadeOutToStopTimer(audioId);
834
872
  asset.unload();
835
873
  audioAssetList.remove(audioId);
874
+ clearTrackedPlaybackState(audioId);
836
875
  call.resolve();
837
876
  } else {
838
877
  call.reject(ERROR_AUDIO_ASSET_MISSING + " - " + audioId);
@@ -989,6 +1028,14 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
989
1028
  JSObject ret = new JSObject();
990
1029
  ret.put("assetId", assetId);
991
1030
  notifyListeners("complete", ret);
1031
+
1032
+ updateTrackedPlaybackState(assetId, PlaybackStateCompat.STATE_STOPPED);
1033
+
1034
+ if (assetId != null && assetId.equals(currentlyPlayingAssetId)) {
1035
+ clearNotification();
1036
+ currentlyPlayingAssetId = null;
1037
+ }
1038
+ notifyPlaybackState(assetId, "complete");
992
1039
  }
993
1040
 
994
1041
  /**
@@ -1240,12 +1287,16 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
1240
1287
  asset.play(time);
1241
1288
  }
1242
1289
 
1290
+ updateTrackedPlaybackState(audioId, PlaybackStateCompat.STATE_PLAYING);
1291
+
1243
1292
  // Update notification if enabled
1244
1293
  if (showNotification) {
1245
1294
  currentlyPlayingAssetId = audioId;
1246
1295
  updateNotification(audioId);
1247
1296
  }
1248
1297
 
1298
+ notifyPlaybackState(audioId, LOOP.equals(action) ? "loop" : "play");
1299
+
1249
1300
  call.resolve();
1250
1301
  } else {
1251
1302
  call.reject("Asset is null: " + audioId);
@@ -1486,6 +1537,14 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
1486
1537
 
1487
1538
  // Notification and MediaSession methods
1488
1539
 
1540
+ static double clampSeekPositionSeconds(double currentPosition, double duration, double deltaSeconds) {
1541
+ double targetPosition = currentPosition + deltaSeconds;
1542
+ if (duration > 0) {
1543
+ return Math.max(0, Math.min(duration, targetPosition));
1544
+ }
1545
+ return Math.max(0, targetPosition);
1546
+ }
1547
+
1489
1548
  private void setupMediaSession() {
1490
1549
  if (mediaSession != null) return;
1491
1550
 
@@ -1508,101 +1567,81 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
1508
1567
  new MediaSessionCompat.Callback() {
1509
1568
  @Override
1510
1569
  public void onPlay() {
1511
- if (currentlyPlayingAssetId != null && audioAssetList.containsKey(currentlyPlayingAssetId)) {
1512
- AudioAsset asset = audioAssetList.get(currentlyPlayingAssetId);
1513
- try {
1514
- if (asset != null && !asset.isPlaying()) {
1515
- asset.resume();
1516
- updatePlaybackState(PlaybackStateCompat.STATE_PLAYING);
1517
- updateNotification(currentlyPlayingAssetId);
1518
- }
1519
- } catch (Exception e) {
1520
- Log.e(TAG, "Error resuming audio from media session", e);
1570
+ handleCurrentMediaAction("resuming", (audioId, asset) -> {
1571
+ if (!asset.isPlaying()) {
1572
+ asset.resume();
1521
1573
  }
1522
- }
1574
+ updateTrackedPlaybackState(audioId, PlaybackStateCompat.STATE_PLAYING);
1575
+ updateNotification(audioId);
1576
+ notifyPlaybackState(audioId, "remotePlay");
1577
+ });
1523
1578
  }
1524
1579
 
1525
1580
  @Override
1526
1581
  public void onPause() {
1527
- if (currentlyPlayingAssetId != null && audioAssetList.containsKey(currentlyPlayingAssetId)) {
1528
- AudioAsset asset = audioAssetList.get(currentlyPlayingAssetId);
1529
- try {
1530
- if (asset != null) {
1531
- asset.pause();
1532
- updatePlaybackState(PlaybackStateCompat.STATE_PAUSED);
1533
- updateNotification(currentlyPlayingAssetId);
1534
- }
1535
- } catch (Exception e) {
1536
- Log.e(TAG, "Error pausing audio from media session", e);
1537
- }
1538
- }
1582
+ handleCurrentMediaAction("pausing", (audioId, asset) -> {
1583
+ asset.pause();
1584
+ updateTrackedPlaybackState(audioId, PlaybackStateCompat.STATE_PAUSED);
1585
+ updateNotification(audioId);
1586
+ notifyPlaybackState(audioId, "remotePause");
1587
+ });
1539
1588
  }
1540
1589
 
1541
1590
  @Override
1542
1591
  public void onStop() {
1543
- if (currentlyPlayingAssetId != null) {
1592
+ String audioId = currentlyPlayingAssetId;
1593
+ runOnMainThread(() -> {
1594
+ if (!isStringValid(audioId)) {
1595
+ return;
1596
+ }
1544
1597
  try {
1545
- stopAudio(currentlyPlayingAssetId, false, 0);
1598
+ stopAudio(audioId, false, 0);
1599
+ updateTrackedPlaybackState(audioId, PlaybackStateCompat.STATE_STOPPED);
1546
1600
  clearNotification();
1547
1601
  currentlyPlayingAssetId = null;
1602
+ notifyPlaybackState(audioId, "remoteStop");
1548
1603
  } catch (Exception e) {
1549
1604
  Log.e(TAG, "Error stopping audio from media session", e);
1550
1605
  }
1551
- }
1606
+ });
1552
1607
  }
1553
1608
 
1554
1609
  @Override
1555
1610
  public void onRewind() {
1556
- if (currentlyPlayingAssetId != null && audioAssetList.containsKey(currentlyPlayingAssetId)) {
1557
- AudioAsset asset = audioAssetList.get(currentlyPlayingAssetId);
1558
- try {
1559
- if (asset != null) {
1560
- // Skip backward 15 seconds
1561
- double currentPosition = asset.getCurrentPosition();
1562
- double newPosition = Math.max(0, currentPosition - 15.0);
1563
- asset.setCurrentPosition(newPosition);
1564
- Log.d(TAG, "Rewind 15s: " + currentPosition + " -> " + newPosition);
1565
- }
1566
- } catch (Exception e) {
1567
- Log.e(TAG, "Error rewinding audio from media session", e);
1568
- }
1569
- }
1611
+ handleCurrentMediaAction("rewinding", (audioId, asset) -> {
1612
+ double currentPosition = asset.getCurrentPosition();
1613
+ double duration = asset.getDuration();
1614
+ double newPosition = clampSeekPositionSeconds(currentPosition, duration, -NOTIFICATION_SKIP_SECONDS);
1615
+ asset.setCurrentPosition(newPosition);
1616
+ updatePlaybackState(resolvePlaybackState(audioId, asset), asset);
1617
+ notifyPlaybackState(audioId, "remoteRewind");
1618
+ Log.d(TAG, "Rewind 15s: " + currentPosition + " -> " + newPosition);
1619
+ });
1570
1620
  }
1571
1621
 
1572
1622
  @Override
1573
1623
  public void onFastForward() {
1574
- if (currentlyPlayingAssetId != null && audioAssetList.containsKey(currentlyPlayingAssetId)) {
1575
- AudioAsset asset = audioAssetList.get(currentlyPlayingAssetId);
1576
- try {
1577
- if (asset != null) {
1578
- // Skip forward 15 seconds
1579
- double currentPosition = asset.getCurrentPosition();
1580
- double duration = asset.getDuration();
1581
- double newPosition = Math.min(duration, currentPosition + 15.0);
1582
- asset.setCurrentPosition(newPosition);
1583
- Log.d(TAG, "Fast forward 15s: " + currentPosition + " -> " + newPosition);
1584
- }
1585
- } catch (Exception e) {
1586
- Log.e(TAG, "Error fast forwarding audio from media session", e);
1587
- }
1588
- }
1624
+ handleCurrentMediaAction("fast forwarding", (audioId, asset) -> {
1625
+ double currentPosition = asset.getCurrentPosition();
1626
+ double duration = asset.getDuration();
1627
+ double newPosition = clampSeekPositionSeconds(currentPosition, duration, NOTIFICATION_SKIP_SECONDS);
1628
+ asset.setCurrentPosition(newPosition);
1629
+ updatePlaybackState(resolvePlaybackState(audioId, asset), asset);
1630
+ notifyPlaybackState(audioId, "remoteFastForward");
1631
+ Log.d(TAG, "Fast forward 15s: " + currentPosition + " -> " + newPosition);
1632
+ });
1589
1633
  }
1590
1634
 
1591
1635
  @Override
1592
1636
  public void onSeekTo(long pos) {
1593
- if (currentlyPlayingAssetId != null && audioAssetList.containsKey(currentlyPlayingAssetId)) {
1594
- AudioAsset asset = audioAssetList.get(currentlyPlayingAssetId);
1595
- try {
1596
- if (asset != null) {
1597
- // Convert milliseconds to seconds
1598
- double positionInSeconds = pos / 1000.0;
1599
- asset.setCurrentPosition(positionInSeconds);
1600
- Log.d(TAG, "Seek to: " + positionInSeconds);
1601
- }
1602
- } catch (Exception e) {
1603
- Log.e(TAG, "Error seeking audio from media session", e);
1604
- }
1605
- }
1637
+ handleCurrentMediaAction("seeking", (audioId, asset) -> {
1638
+ double duration = asset.getDuration();
1639
+ double positionInSeconds = clampSeekPositionSeconds(0, duration, pos / 1000.0);
1640
+ asset.setCurrentPosition(positionInSeconds);
1641
+ updatePlaybackState(resolvePlaybackState(audioId, asset), asset);
1642
+ notifyPlaybackState(audioId, "remoteSeek");
1643
+ Log.d(TAG, "Seek to: " + positionInSeconds);
1644
+ });
1606
1645
  }
1607
1646
  }
1608
1647
  );
@@ -1622,7 +1661,10 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
1622
1661
  }
1623
1662
 
1624
1663
  private void updateNotification(String audioId) {
1625
- if (mediaSession == null) return;
1664
+ if (mediaSession == null || !isStringValid(audioId)) return;
1665
+
1666
+ AudioAsset asset = audioAssetList.get(audioId);
1667
+ int playbackState = resolvePlaybackState(audioId, asset);
1626
1668
 
1627
1669
  Map<String, String> metadata = notificationMetadataMap.get(audioId);
1628
1670
  String title = metadata != null && metadata.containsKey("title") ? metadata.get("title") : "Playing";
@@ -1635,38 +1677,37 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
1635
1677
  metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title);
1636
1678
  metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist);
1637
1679
  metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album);
1680
+ long durationMs = getPlaybackDurationMs(asset);
1681
+ if (durationMs > 0) {
1682
+ metadataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMs);
1683
+ }
1684
+
1685
+ updatePlaybackState(playbackState, asset);
1638
1686
 
1639
1687
  // Load artwork if provided
1640
1688
  if (artworkUrl != null) {
1689
+ String targetAudioId = audioId;
1641
1690
  loadArtwork(artworkUrl, (bitmap) -> {
1691
+ if (mediaSession == null || !targetAudioId.equals(currentlyPlayingAssetId)) {
1692
+ return;
1693
+ }
1694
+
1695
+ AudioAsset currentAsset = audioAssetList.get(targetAudioId);
1696
+ int currentPlaybackState = resolvePlaybackState(targetAudioId, currentAsset);
1642
1697
  if (bitmap != null) {
1643
1698
  metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap);
1699
+ metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, bitmap);
1644
1700
  }
1645
1701
  mediaSession.setMetadata(metadataBuilder.build());
1646
- showNotification(title, artist);
1702
+ showNotification(title, artist, bitmap, currentPlaybackState == PlaybackStateCompat.STATE_PLAYING);
1647
1703
  });
1648
1704
  } else {
1649
1705
  mediaSession.setMetadata(metadataBuilder.build());
1650
- showNotification(title, artist);
1706
+ showNotification(title, artist, null, playbackState == PlaybackStateCompat.STATE_PLAYING);
1651
1707
  }
1652
-
1653
- updatePlaybackState(PlaybackStateCompat.STATE_PLAYING);
1654
1708
  }
1655
1709
 
1656
- private void showNotification(String title, String artist) {
1657
- // Determine if currently playing
1658
- boolean isPlaying = false;
1659
- if (currentlyPlayingAssetId != null && audioAssetList.containsKey(currentlyPlayingAssetId)) {
1660
- AudioAsset asset = audioAssetList.get(currentlyPlayingAssetId);
1661
- if (asset != null) {
1662
- try {
1663
- isPlaying = asset.isPlaying();
1664
- } catch (Exception e) {
1665
- Log.e(TAG, "Error checking playback state", e);
1666
- }
1667
- }
1668
- }
1669
-
1710
+ private void showNotification(String title, String artist, Bitmap artwork, boolean isPlaying) {
1670
1711
  // Build notification with proper action order: Rewind, Play/Pause, Fast Forward
1671
1712
  // Use MediaButtonReceiver to properly wire actions to MediaSession callbacks
1672
1713
  NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(getContext(), CHANNEL_ID)
@@ -1674,6 +1715,7 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
1674
1715
  .setContentTitle(title)
1675
1716
  .setContentText(artist)
1676
1717
  .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
1718
+ .setOngoing(isPlaying)
1677
1719
  // Add actions BEFORE setStyle() for proper wiring
1678
1720
  .addAction(
1679
1721
  new NotificationCompat.Action.Builder(
@@ -1713,6 +1755,15 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
1713
1755
  .setPriority(NotificationCompat.PRIORITY_LOW)
1714
1756
  .setOnlyAlertOnce(true);
1715
1757
 
1758
+ if (artwork != null) {
1759
+ notificationBuilder.setLargeIcon(artwork);
1760
+ }
1761
+
1762
+ PendingIntent contentIntent = getNotificationContentIntent();
1763
+ if (contentIntent != null) {
1764
+ notificationBuilder.setContentIntent(contentIntent);
1765
+ }
1766
+
1716
1767
  NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getContext());
1717
1768
  notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
1718
1769
  }
@@ -1722,15 +1773,20 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
1722
1773
  notificationManager.cancel(NOTIFICATION_ID);
1723
1774
 
1724
1775
  if (mediaSession != null) {
1725
- updatePlaybackState(PlaybackStateCompat.STATE_STOPPED);
1776
+ updatePlaybackState(PlaybackStateCompat.STATE_STOPPED, null);
1726
1777
  }
1727
1778
  }
1728
1779
 
1729
1780
  private void updatePlaybackState(int state) {
1781
+ updatePlaybackState(state, getCurrentPlaybackAsset());
1782
+ }
1783
+
1784
+ private void updatePlaybackState(int state, AudioAsset asset) {
1730
1785
  if (mediaSession == null) return;
1731
1786
 
1787
+ long positionMs = getPlaybackPositionMs(asset);
1732
1788
  PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder()
1733
- .setState(state, 0, state == PlaybackStateCompat.STATE_PLAYING ? 1.0f : 0.0f)
1789
+ .setState(state, positionMs, state == PlaybackStateCompat.STATE_PLAYING ? 1.0f : 0.0f, SystemClock.elapsedRealtime())
1734
1790
  .setActions(
1735
1791
  PlaybackStateCompat.ACTION_PLAY |
1736
1792
  PlaybackStateCompat.ACTION_PAUSE |
@@ -1742,6 +1798,167 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
1742
1798
  mediaSession.setPlaybackState(stateBuilder.build());
1743
1799
  }
1744
1800
 
1801
+ private void syncCurrentPlaybackState(String reason) {
1802
+ if (!isStringValid(currentlyPlayingAssetId)) {
1803
+ return;
1804
+ }
1805
+ updateTrackedPlaybackState(currentlyPlayingAssetId, resolvePlaybackState(currentlyPlayingAssetId, getCurrentPlaybackAsset()));
1806
+ if (showNotification) {
1807
+ updateNotification(currentlyPlayingAssetId);
1808
+ }
1809
+ notifyPlaybackState(currentlyPlayingAssetId, reason);
1810
+ }
1811
+
1812
+ private void handleCurrentMediaAction(String verb, MediaSessionAction action) {
1813
+ String audioId = currentlyPlayingAssetId;
1814
+ runOnMainThread(() -> {
1815
+ if (!isStringValid(audioId) || !audioAssetList.containsKey(audioId)) {
1816
+ return;
1817
+ }
1818
+
1819
+ AudioAsset asset = audioAssetList.get(audioId);
1820
+ if (asset == null) {
1821
+ return;
1822
+ }
1823
+
1824
+ try {
1825
+ action.run(audioId, asset);
1826
+ } catch (Exception e) {
1827
+ Log.e(TAG, "Error " + verb + " audio from media session", e);
1828
+ }
1829
+ });
1830
+ }
1831
+
1832
+ private void runOnMainThread(Runnable action) {
1833
+ if (getActivity() != null) {
1834
+ getActivity().runOnUiThread(action);
1835
+ } else {
1836
+ new Handler(Looper.getMainLooper()).post(action);
1837
+ }
1838
+ }
1839
+
1840
+ private AudioAsset getCurrentPlaybackAsset() {
1841
+ if (!isStringValid(currentlyPlayingAssetId)) {
1842
+ return null;
1843
+ }
1844
+ return audioAssetList.get(currentlyPlayingAssetId);
1845
+ }
1846
+
1847
+ private int resolvePlaybackState(String audioId, AudioAsset asset) {
1848
+ if (!isStringValid(audioId)) {
1849
+ return PlaybackStateCompat.STATE_STOPPED;
1850
+ }
1851
+
1852
+ if (asset != null) {
1853
+ try {
1854
+ if (asset.isPlaying()) {
1855
+ return PlaybackStateCompat.STATE_PLAYING;
1856
+ }
1857
+ } catch (Exception e) {
1858
+ Log.e(TAG, "Error resolving playback state", e);
1859
+ }
1860
+ }
1861
+
1862
+ Integer trackedState = playbackStateByAssetId.get(audioId);
1863
+ if (trackedState != null) {
1864
+ return trackedState;
1865
+ }
1866
+
1867
+ if (audioId.equals(currentlyPlayingAssetId)) {
1868
+ return PlaybackStateCompat.STATE_PAUSED;
1869
+ }
1870
+
1871
+ return PlaybackStateCompat.STATE_STOPPED;
1872
+ }
1873
+
1874
+ private long getPlaybackPositionMs(AudioAsset asset) {
1875
+ if (asset == null) {
1876
+ return 0;
1877
+ }
1878
+
1879
+ try {
1880
+ return Math.max(0, Math.round(asset.getCurrentPosition() * 1000));
1881
+ } catch (Exception e) {
1882
+ Log.e(TAG, "Error reading playback position", e);
1883
+ return 0;
1884
+ }
1885
+ }
1886
+
1887
+ private long getPlaybackDurationMs(AudioAsset asset) {
1888
+ if (asset == null) {
1889
+ return 0;
1890
+ }
1891
+
1892
+ try {
1893
+ return Math.max(0, Math.round(asset.getDuration() * 1000));
1894
+ } catch (Exception e) {
1895
+ Log.e(TAG, "Error reading playback duration", e);
1896
+ return 0;
1897
+ }
1898
+ }
1899
+
1900
+ private void notifyPlaybackState(String audioId, String reason) {
1901
+ if (!isStringValid(audioId)) {
1902
+ return;
1903
+ }
1904
+
1905
+ AudioAsset asset = audioAssetList.get(audioId);
1906
+ int playbackState = resolvePlaybackState(audioId, asset);
1907
+
1908
+ JSObject ret = new JSObject();
1909
+ ret.put("assetId", audioId);
1910
+ ret.put("state", playbackStateToString(playbackState));
1911
+ ret.put("reason", reason);
1912
+ ret.put("isPlaying", playbackState == PlaybackStateCompat.STATE_PLAYING);
1913
+
1914
+ if (asset != null) {
1915
+ ret.put("currentTime", getPlaybackPositionMs(asset) / 1000.0);
1916
+ ret.put("duration", getPlaybackDurationMs(asset) / 1000.0);
1917
+ }
1918
+
1919
+ notifyListeners("playbackState", ret);
1920
+ }
1921
+
1922
+ private void updateTrackedPlaybackState(String audioId, int playbackState) {
1923
+ if (!isStringValid(audioId)) {
1924
+ return;
1925
+ }
1926
+ playbackStateByAssetId.put(audioId, playbackState);
1927
+ }
1928
+
1929
+ private void clearTrackedPlaybackState(String audioId) {
1930
+ if (!isStringValid(audioId)) {
1931
+ return;
1932
+ }
1933
+ playbackStateByAssetId.remove(audioId);
1934
+ }
1935
+
1936
+ private String playbackStateToString(int playbackState) {
1937
+ if (playbackState == PlaybackStateCompat.STATE_PLAYING) {
1938
+ return "playing";
1939
+ }
1940
+ if (playbackState == PlaybackStateCompat.STATE_PAUSED) {
1941
+ return "paused";
1942
+ }
1943
+ return "stopped";
1944
+ }
1945
+
1946
+ private PendingIntent getNotificationContentIntent() {
1947
+ Intent launchIntent = getContext().getPackageManager().getLaunchIntentForPackage(getContext().getPackageName());
1948
+ if (launchIntent == null) {
1949
+ return null;
1950
+ }
1951
+
1952
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
1953
+
1954
+ int flags = PendingIntent.FLAG_UPDATE_CURRENT;
1955
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1956
+ flags |= PendingIntent.FLAG_IMMUTABLE;
1957
+ }
1958
+
1959
+ return PendingIntent.getActivity(getContext(), NOTIFICATION_ID, launchIntent, flags);
1960
+ }
1961
+
1745
1962
  private void loadArtwork(String urlString, ArtworkCallback callback) {
1746
1963
  new Thread(() -> {
1747
1964
  try {
@@ -1797,4 +2014,8 @@ public class NativeAudio extends Plugin implements AudioManager.OnAudioFocusChan
1797
2014
  interface ArtworkCallback {
1798
2015
  void onArtworkLoaded(Bitmap bitmap);
1799
2016
  }
2017
+
2018
+ private interface MediaSessionAction {
2019
+ void run(String audioId, AudioAsset asset) throws Exception;
2020
+ }
1800
2021
  }