@javascriptcommon/react-native-track-player 1.2.9 → 1.2.24
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/android/build.gradle +61 -4
- package/android/src/main/AndroidManifest.xml +2 -4
- package/android/src/main/ic_home-playstore.png +0 -0
- package/android/src/main/ic_repeat-playstore.png +0 -0
- package/android/src/main/ic_repeat_50-playstore.png +0 -0
- package/android/src/main/ic_shuffle-playstore.png +0 -0
- package/android/src/main/ic_shuffle_50-playstore.png +0 -0
- package/android/src/main/ic_shuffle_sm-playstore.png +0 -0
- package/android/src/main/ic_stop-playstore.png +0 -0
- package/android/src/main/ic_test-playstore.png +0 -0
- package/android/src/main/java/com/guichaguri/trackplayer/{service/HeadlessJsMediaService.java → HeadlessJsMediaService.java} +83 -32
- package/android/src/main/java/com/guichaguri/trackplayer/TrackPlayer.kt +25 -0
- package/android/src/main/java/com/guichaguri/trackplayer/extensions/AudioPlayerStateExt.kt +19 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/event/EventHolder.kt +30 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/event/NotificationEventHolder.kt +20 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/event/PlayerEventHolder.kt +111 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/AAMediaSessionCallback.kt +10 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/AudioContentType.kt +10 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/AudioItem.kt +66 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/AudioItemTransitionReason.kt +33 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/AudioPlayerState.kt +30 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/BufferConfig.kt +8 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/CacheConfig.kt +17 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/Capability.kt +19 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/FocusChangeData.kt +3 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/MediaSessionCallback.kt +17 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/NotificationConfig.kt +43 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/NotificationMetadata.kt +8 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/NotificationState.kt +8 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PlayWhenReadyChangeData.kt +5 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PlaybackEndedReason.kt +5 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PlaybackError.kt +6 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PlaybackMetadata.kt +200 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PlayerConfig.kt +33 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PlayerOptions.kt +9 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PositionChangedReason.kt +39 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/QueuedPlayerOptions.kt +49 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/notification/NotificationManager.kt +678 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/players/AudioPlayer.kt +10 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/players/BaseAudioPlayer.kt +864 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/players/QueuedAudioPlayer.kt +269 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/players/components/MediaSourceExt.kt +35 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/players/components/PlayerCache.kt +26 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/utils/Utils.kt +12 -0
- package/android/src/main/java/com/guichaguri/trackplayer/model/MetadataAdapter.kt +224 -0
- package/android/src/main/java/com/guichaguri/trackplayer/model/State.kt +13 -0
- package/android/src/main/java/com/guichaguri/trackplayer/model/Track.kt +120 -0
- package/android/src/main/java/com/guichaguri/trackplayer/model/TrackAudioItem.kt +19 -0
- package/android/src/main/java/com/guichaguri/trackplayer/model/TrackType.kt +11 -0
- package/android/src/main/java/com/guichaguri/trackplayer/module/AutoConnectionDetector.kt +151 -0
- package/android/src/main/java/com/guichaguri/trackplayer/module/MusicEvents.kt +66 -0
- package/android/src/main/java/com/guichaguri/trackplayer/module/MusicModule.kt +1192 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/BundleUtils.kt +117 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/MusicBinder.kt +31 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/MusicManager.kt +347 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/MusicService.kt +1268 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/Utils.kt +228 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/metadata/ButtonEvents.kt +141 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/metadata/MetadataManager.kt +396 -0
- package/android/src/main/res/drawable-hdpi/ic_home.png +0 -0
- package/android/src/main/res/drawable-mdpi/ic_home.png +0 -0
- package/android/src/main/res/drawable-xhdpi/ic_home.png +0 -0
- package/android/src/main/res/drawable-xxhdpi/ic_home.png +0 -0
- package/android/src/main/res/drawable-xxxhdpi/ic_home.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_arrow_down_circle_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_clock_now_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_close_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_heart_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_heart_outlined_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_repeat_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_repeat_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_shuffle_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_shuffle_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_arrow_down_circle_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_clock_now_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_close_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_heart_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_heart_outlined_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_repeat_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_repeat_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_shuffle_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_shuffle_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_arrow_down_circle_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_clock_now_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_close_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_heart_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_heart_outlined_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_repeat_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_repeat_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_shuffle_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_shuffle_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_arrow_down_circle_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_clock_now_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_close_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_heart_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_heart_outlined_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_repeat_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_repeat_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_shuffle_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_shuffle_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_arrow_down_circle_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_clock_now_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_close_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_heart_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_heart_outlined_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_repeat_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_repeat_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_shuffle_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_shuffle_on_foreground.png +0 -0
- package/android/src/main/res/raw/silent_5_seconds.mp3 +0 -0
- package/android/src/main/res/strings.xml +6 -0
- package/android/src/main/res/values/strings.xml +6 -0
- package/index.d.ts +62 -1
- package/lib/index.js +10 -9
- package/package.json +1 -1
- package/android/src/main/java/com/guichaguri/trackplayer/TrackPlayer.java +0 -28
- package/android/src/main/java/com/guichaguri/trackplayer/module/MusicEvents.java +0 -55
- package/android/src/main/java/com/guichaguri/trackplayer/module/MusicModule.java +0 -298
- package/android/src/main/java/com/guichaguri/trackplayer/service/MusicBinder.java +0 -47
- package/android/src/main/java/com/guichaguri/trackplayer/service/MusicManager.java +0 -383
- package/android/src/main/java/com/guichaguri/trackplayer/service/MusicService.java +0 -271
- package/android/src/main/java/com/guichaguri/trackplayer/service/Utils.java +0 -243
- package/android/src/main/java/com/guichaguri/trackplayer/service/metadata/ButtonEvents.java +0 -148
- package/android/src/main/java/com/guichaguri/trackplayer/service/metadata/MetadataManager.java +0 -379
- package/android/src/main/java/com/guichaguri/trackplayer/service/models/Track.java +0 -141
- package/android/src/main/java/com/guichaguri/trackplayer/service/models/TrackType.java +0 -35
- package/android/src/main/res/drawable-hdpi/ic_logo.png +0 -0
- package/android/src/main/res/drawable-mdpi/ic_logo.png +0 -0
- package/android/src/main/res/drawable-xhdpi/ic_logo.png +0 -0
- package/android/src/main/res/drawable-xxhdpi/ic_logo.png +0 -0
- package/android/src/main/res/drawable-xxxhdpi/ic_logo.png +0 -0
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
package com.guichaguri.trackplayer.service;
|
|
2
|
-
|
|
3
|
-
import android.annotation.SuppressLint;
|
|
4
|
-
import android.content.BroadcastReceiver;
|
|
5
|
-
import android.content.Context;
|
|
6
|
-
import android.content.Intent;
|
|
7
|
-
import android.content.IntentFilter;
|
|
8
|
-
import android.media.AudioAttributes;
|
|
9
|
-
import android.media.AudioDeviceInfo;
|
|
10
|
-
import android.media.AudioFocusRequest;
|
|
11
|
-
import android.media.AudioManager;
|
|
12
|
-
import android.media.AudioManager.OnAudioFocusChangeListener;
|
|
13
|
-
import android.net.wifi.WifiManager;
|
|
14
|
-
import android.net.wifi.WifiManager.WifiLock;
|
|
15
|
-
import android.os.Build;
|
|
16
|
-
import android.os.Bundle;
|
|
17
|
-
import android.os.Handler;
|
|
18
|
-
import android.os.PowerManager;
|
|
19
|
-
import android.os.PowerManager.WakeLock;
|
|
20
|
-
import androidx.annotation.RequiresApi;
|
|
21
|
-
|
|
22
|
-
import android.support.v4.media.session.PlaybackStateCompat;
|
|
23
|
-
import android.util.Log;
|
|
24
|
-
import com.guichaguri.trackplayer.module.MusicEvents;
|
|
25
|
-
import com.guichaguri.trackplayer.service.metadata.MetadataManager;
|
|
26
|
-
import com.guichaguri.trackplayer.service.models.Track;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @author Guichaguri
|
|
30
|
-
*/
|
|
31
|
-
public class MusicManager implements OnAudioFocusChangeListener {
|
|
32
|
-
|
|
33
|
-
private final MusicService service;
|
|
34
|
-
|
|
35
|
-
private final WakeLock wakeLock;
|
|
36
|
-
private final WifiLock wifiLock;
|
|
37
|
-
|
|
38
|
-
private MetadataManager metadata;
|
|
39
|
-
|
|
40
|
-
private Track currentTrack;
|
|
41
|
-
|
|
42
|
-
private int state;
|
|
43
|
-
private int previousState;
|
|
44
|
-
private long position;
|
|
45
|
-
private long bufferedPosition;
|
|
46
|
-
|
|
47
|
-
@RequiresApi(26)
|
|
48
|
-
private AudioFocusRequest focus = null;
|
|
49
|
-
private boolean hasAudioFocus = false;
|
|
50
|
-
private boolean wasDucking = false;
|
|
51
|
-
|
|
52
|
-
private BroadcastReceiver noisyReceiver = new BroadcastReceiver() {
|
|
53
|
-
@Override
|
|
54
|
-
public void onReceive(Context context, Intent intent) {
|
|
55
|
-
service.emit(MusicEvents.BUTTON_PAUSE, null);
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
private boolean receivingNoisyEvents = false;
|
|
59
|
-
|
|
60
|
-
private boolean stopWithApp = false;
|
|
61
|
-
private boolean alwaysPauseOnInterruption = false;
|
|
62
|
-
|
|
63
|
-
@SuppressLint("InvalidWakeLockTag")
|
|
64
|
-
public MusicManager(MusicService service) {
|
|
65
|
-
this.service = service;
|
|
66
|
-
this.metadata = new MetadataManager(service, this);
|
|
67
|
-
|
|
68
|
-
PowerManager powerManager = (PowerManager)service.getSystemService(Context.POWER_SERVICE);
|
|
69
|
-
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "track-player-wake-lock");
|
|
70
|
-
wakeLock.setReferenceCounted(false);
|
|
71
|
-
|
|
72
|
-
// Android 7: Use the application context here to prevent any memory leaks
|
|
73
|
-
WifiManager wifiManager = (WifiManager)service.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
|
74
|
-
wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "track-player-wifi-lock");
|
|
75
|
-
wifiLock.setReferenceCounted(false);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
public Track getCurrentTrack(){return currentTrack;}
|
|
79
|
-
public void setCurrentTrack(Track track){currentTrack = track;}
|
|
80
|
-
|
|
81
|
-
public boolean shouldStopWithApp() {
|
|
82
|
-
return stopWithApp;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
public void setStopWithApp(boolean stopWithApp) {
|
|
86
|
-
this.stopWithApp = stopWithApp;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
public void setAlwaysPauseOnInterruption(boolean alwaysPauseOnInterruption) {
|
|
90
|
-
this.alwaysPauseOnInterruption = alwaysPauseOnInterruption;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
public MetadataManager getMetadata() {
|
|
94
|
-
return metadata;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
public Handler getHandler() {
|
|
98
|
-
return service.handler;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
public long getState(){
|
|
102
|
-
return state;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
public void setState(int state, long position){
|
|
106
|
-
this.state = state;
|
|
107
|
-
if(position != -1) {
|
|
108
|
-
this.position = position;
|
|
109
|
-
this.bufferedPosition = position;
|
|
110
|
-
}
|
|
111
|
-
onPlayerStateChanged();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
@SuppressLint("WakelockTimeout")
|
|
115
|
-
public void onPlay() {
|
|
116
|
-
Log.d(Utils.LOG, "onPlay");
|
|
117
|
-
/*if(playback == null) return;
|
|
118
|
-
|
|
119
|
-
Track track = playback.getCurrentTrack();
|
|
120
|
-
if(track == null) return;
|
|
121
|
-
|
|
122
|
-
if(!playback.isRemote()) {
|
|
123
|
-
requestFocus();
|
|
124
|
-
|
|
125
|
-
if(!receivingNoisyEvents) {
|
|
126
|
-
receivingNoisyEvents = true;
|
|
127
|
-
service.registerReceiver(noisyReceiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if(!wakeLock.isHeld()) wakeLock.acquire();
|
|
131
|
-
|
|
132
|
-
if(!Utils.isLocal(track.uri)) {
|
|
133
|
-
if(!wifiLock.isHeld()) wifiLock.acquire();
|
|
134
|
-
}
|
|
135
|
-
}*/
|
|
136
|
-
|
|
137
|
-
requestFocus();
|
|
138
|
-
|
|
139
|
-
if(!receivingNoisyEvents) {
|
|
140
|
-
receivingNoisyEvents = true;
|
|
141
|
-
service.registerReceiver(noisyReceiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if(!wakeLock.isHeld()) wakeLock.acquire();
|
|
145
|
-
|
|
146
|
-
/*if(!Utils.isLocal(currentTrack.uri)) {
|
|
147
|
-
if(!wifiLock.isHeld()) wifiLock.acquire();
|
|
148
|
-
}*/
|
|
149
|
-
|
|
150
|
-
metadata.setActive(true);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
public void onPause() {
|
|
154
|
-
Log.d(Utils.LOG, "onPause");
|
|
155
|
-
|
|
156
|
-
// Unregisters the noisy receiver
|
|
157
|
-
if(receivingNoisyEvents) {
|
|
158
|
-
try {
|
|
159
|
-
service.unregisterReceiver(noisyReceiver);
|
|
160
|
-
receivingNoisyEvents = false;
|
|
161
|
-
}catch(Exception e){ }
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Release the wake and the wifi locks
|
|
165
|
-
if(wakeLock.isHeld()) wakeLock.release();
|
|
166
|
-
if(wifiLock.isHeld()) wifiLock.release();
|
|
167
|
-
|
|
168
|
-
metadata.setActive(true);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
public void onStop() {
|
|
172
|
-
Log.d(Utils.LOG, "onStop");
|
|
173
|
-
|
|
174
|
-
// Unregisters the noisy receiver
|
|
175
|
-
if(receivingNoisyEvents) {
|
|
176
|
-
try {
|
|
177
|
-
service.unregisterReceiver(noisyReceiver);
|
|
178
|
-
receivingNoisyEvents = false;
|
|
179
|
-
}catch(Exception e){ }
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Release the wake and the wifi locks
|
|
183
|
-
if(wakeLock.isHeld()) wakeLock.release();
|
|
184
|
-
if(wifiLock.isHeld()) wifiLock.release();
|
|
185
|
-
|
|
186
|
-
abandonFocus();
|
|
187
|
-
|
|
188
|
-
metadata.setActive(false);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
public void onPlayerStateChanged() {
|
|
192
|
-
|
|
193
|
-
if(Utils.isPlaying(state) && !Utils.isPlaying(previousState)) {
|
|
194
|
-
this.onPlay();
|
|
195
|
-
} else if(Utils.isPaused(state) && !Utils.isPaused(previousState)) {
|
|
196
|
-
this.onPause();
|
|
197
|
-
} else if(Utils.isStopped(state) && !Utils.isStopped(previousState)) {
|
|
198
|
-
this.onStop();
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
this.onStateChange(state, position, bufferedPosition);
|
|
202
|
-
previousState = state;
|
|
203
|
-
|
|
204
|
-
if(state == PlaybackStateCompat.STATE_STOPPED) {
|
|
205
|
-
this.onEnd(getCurrentTrack(), 0);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
public void onStateChange(int state, long position, long bufferedPosition) {
|
|
210
|
-
Log.d(Utils.LOG, "onStateChange");
|
|
211
|
-
|
|
212
|
-
Bundle bundle = new Bundle();
|
|
213
|
-
bundle.putInt("state", state);
|
|
214
|
-
service.emit(MusicEvents.PLAYBACK_STATE, bundle);
|
|
215
|
-
metadata.updatePlayback(state, position, bufferedPosition, 1);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
public void onTrackUpdate(Track previous, long prevPos, Track next) {
|
|
219
|
-
Log.d(Utils.LOG, "onTrackUpdate");
|
|
220
|
-
|
|
221
|
-
if(next != null) metadata.updateMetadata(next);
|
|
222
|
-
|
|
223
|
-
Bundle bundle = new Bundle();
|
|
224
|
-
bundle.putString("track", previous != null ? previous.id : null);
|
|
225
|
-
bundle.putDouble("position", Utils.toSeconds(prevPos));
|
|
226
|
-
bundle.putString("nextTrack", next != null ? next.id : null);
|
|
227
|
-
service.emit(MusicEvents.PLAYBACK_TRACK_CHANGED, bundle);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
public void onReset() {
|
|
231
|
-
metadata.removeNotifications();
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
public void onEnd(Track previous, long prevPos) {
|
|
235
|
-
Log.d(Utils.LOG, "onEnd");
|
|
236
|
-
|
|
237
|
-
Bundle bundle = new Bundle();
|
|
238
|
-
bundle.putString("track", previous != null ? previous.id : null);
|
|
239
|
-
bundle.putDouble("position", Utils.toSeconds(prevPos));
|
|
240
|
-
service.emit(MusicEvents.PLAYBACK_QUEUE_ENDED, bundle);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
public void onMetadataReceived(String source, String title, String url, String artist, String album, String date, String genre) {
|
|
244
|
-
Log.d(Utils.LOG, "onMetadataReceived: " + source);
|
|
245
|
-
|
|
246
|
-
Bundle bundle = new Bundle();
|
|
247
|
-
bundle.putString("source", source);
|
|
248
|
-
bundle.putString("title", title);
|
|
249
|
-
bundle.putString("url", url);
|
|
250
|
-
bundle.putString("artist", artist);
|
|
251
|
-
bundle.putString("album", album);
|
|
252
|
-
bundle.putString("date", date);
|
|
253
|
-
bundle.putString("genre", genre);
|
|
254
|
-
service.emit(MusicEvents.PLAYBACK_METADATA, bundle);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
public void onError(String code, String error) {
|
|
258
|
-
Log.d(Utils.LOG, "onError");
|
|
259
|
-
Log.e(Utils.LOG, "Playback error: " + code + " - " + error);
|
|
260
|
-
|
|
261
|
-
Bundle bundle = new Bundle();
|
|
262
|
-
bundle.putString("code", code);
|
|
263
|
-
bundle.putString("message", error);
|
|
264
|
-
service.emit(MusicEvents.PLAYBACK_ERROR, bundle);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
@Override
|
|
268
|
-
public void onAudioFocusChange(int focus) {
|
|
269
|
-
Log.d(Utils.LOG, "onDuck");
|
|
270
|
-
|
|
271
|
-
boolean permanent = false;
|
|
272
|
-
boolean paused = false;
|
|
273
|
-
boolean ducking = false;
|
|
274
|
-
|
|
275
|
-
switch(focus) {
|
|
276
|
-
case AudioManager.AUDIOFOCUS_GAIN:
|
|
277
|
-
paused = false;
|
|
278
|
-
break;
|
|
279
|
-
case AudioManager.AUDIOFOCUS_LOSS:
|
|
280
|
-
permanent = true;
|
|
281
|
-
paused = true;
|
|
282
|
-
abandonFocus();
|
|
283
|
-
break;
|
|
284
|
-
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
|
285
|
-
paused = true;
|
|
286
|
-
break;
|
|
287
|
-
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
|
|
288
|
-
if (alwaysPauseOnInterruption)
|
|
289
|
-
paused = true;
|
|
290
|
-
else
|
|
291
|
-
ducking = true;
|
|
292
|
-
break;
|
|
293
|
-
default:
|
|
294
|
-
break;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (ducking) {
|
|
298
|
-
//playback.setVolumeMultiplier(0.5F);
|
|
299
|
-
wasDucking = true;
|
|
300
|
-
} else if (wasDucking) {
|
|
301
|
-
//playback.setVolumeMultiplier(1.0F);
|
|
302
|
-
wasDucking = false;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
Bundle bundle = new Bundle();
|
|
306
|
-
bundle.putBoolean("permanent", permanent);
|
|
307
|
-
bundle.putBoolean("paused", paused);
|
|
308
|
-
bundle.putBoolean("ducking", ducking);
|
|
309
|
-
service.emit(MusicEvents.BUTTON_DUCK, bundle);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
private void requestFocus() {
|
|
313
|
-
if(hasAudioFocus) return;
|
|
314
|
-
Log.d(Utils.LOG, "Requesting audio focus...");
|
|
315
|
-
|
|
316
|
-
AudioManager manager = (AudioManager)service.getSystemService(Context.AUDIO_SERVICE);
|
|
317
|
-
int r;
|
|
318
|
-
|
|
319
|
-
if(manager == null) {
|
|
320
|
-
r = AudioManager.AUDIOFOCUS_REQUEST_FAILED;
|
|
321
|
-
} else if(Build.VERSION.SDK_INT >= 26) {
|
|
322
|
-
focus = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
|
323
|
-
.setOnAudioFocusChangeListener(this)
|
|
324
|
-
.setAudioAttributes(new AudioAttributes.Builder()
|
|
325
|
-
.setUsage(AudioAttributes.USAGE_MEDIA)
|
|
326
|
-
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
|
327
|
-
.build())
|
|
328
|
-
.setWillPauseWhenDucked(alwaysPauseOnInterruption)
|
|
329
|
-
.build();
|
|
330
|
-
|
|
331
|
-
r = manager.requestAudioFocus(focus);
|
|
332
|
-
} else {
|
|
333
|
-
//noinspection deprecation
|
|
334
|
-
r = manager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
hasAudioFocus = r == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
private void abandonFocus() {
|
|
341
|
-
if(!hasAudioFocus) return;
|
|
342
|
-
Log.d(Utils.LOG, "Abandoning audio focus...");
|
|
343
|
-
|
|
344
|
-
AudioManager manager = (AudioManager)service.getSystemService(Context.AUDIO_SERVICE);
|
|
345
|
-
int r;
|
|
346
|
-
|
|
347
|
-
if(manager == null) {
|
|
348
|
-
r = AudioManager.AUDIOFOCUS_REQUEST_FAILED;
|
|
349
|
-
} else if(Build.VERSION.SDK_INT >= 26) {
|
|
350
|
-
r = manager.abandonAudioFocusRequest(focus);
|
|
351
|
-
} else {
|
|
352
|
-
//noinspection deprecation
|
|
353
|
-
r = manager.abandonAudioFocus(this);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
hasAudioFocus = r != AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
public void destroy(boolean intentToStop) {
|
|
360
|
-
Log.d(Utils.LOG, "Releasing service resources...");
|
|
361
|
-
|
|
362
|
-
// Disable audio focus
|
|
363
|
-
abandonFocus();
|
|
364
|
-
|
|
365
|
-
// Stop receiving audio becoming noisy events
|
|
366
|
-
if(receivingNoisyEvents) {
|
|
367
|
-
try {
|
|
368
|
-
service.unregisterReceiver(noisyReceiver);
|
|
369
|
-
receivingNoisyEvents = false;
|
|
370
|
-
}catch(Exception e){ }
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Release the playback resources
|
|
374
|
-
//if(playback != null) playback.destroy();
|
|
375
|
-
|
|
376
|
-
// Release the metadata resources
|
|
377
|
-
metadata.destroy(intentToStop);
|
|
378
|
-
|
|
379
|
-
// Release the locks
|
|
380
|
-
if(wifiLock.isHeld()) wifiLock.release();
|
|
381
|
-
if(wakeLock.isHeld()) wakeLock.release();
|
|
382
|
-
}
|
|
383
|
-
}
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
package com.guichaguri.trackplayer.service;
|
|
2
|
-
|
|
3
|
-
import android.app.Notification;
|
|
4
|
-
import android.app.NotificationChannel;
|
|
5
|
-
import android.app.NotificationManager;
|
|
6
|
-
import android.content.Context;
|
|
7
|
-
import android.content.Intent;
|
|
8
|
-
import android.content.SharedPreferences;
|
|
9
|
-
import android.content.pm.ServiceInfo;
|
|
10
|
-
import android.graphics.Bitmap;
|
|
11
|
-
import android.graphics.BitmapFactory;
|
|
12
|
-
import android.os.Build;
|
|
13
|
-
import android.os.Bundle;
|
|
14
|
-
import android.os.Handler;
|
|
15
|
-
import android.os.IBinder;
|
|
16
|
-
import android.preference.PreferenceManager;
|
|
17
|
-
import android.support.v4.media.MediaBrowserCompat;
|
|
18
|
-
import android.support.v4.media.RatingCompat;
|
|
19
|
-
import android.util.Log;
|
|
20
|
-
import android.view.KeyEvent;
|
|
21
|
-
|
|
22
|
-
import androidx.annotation.NonNull;
|
|
23
|
-
import androidx.core.app.NotificationCompat;
|
|
24
|
-
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|
25
|
-
import androidx.media.session.MediaButtonReceiver;
|
|
26
|
-
|
|
27
|
-
import com.facebook.react.HeadlessJsTaskService;
|
|
28
|
-
import com.facebook.react.ReactInstanceManager;
|
|
29
|
-
import com.facebook.react.bridge.Arguments;
|
|
30
|
-
import com.facebook.react.bridge.ReactContext;
|
|
31
|
-
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
|
|
32
|
-
import com.guichaguri.trackplayer.R;
|
|
33
|
-
import com.guichaguri.trackplayer.service.metadata.ButtonEvents;
|
|
34
|
-
import com.guichaguri.trackplayer.service.models.Track;
|
|
35
|
-
|
|
36
|
-
import javax.annotation.Nullable;
|
|
37
|
-
|
|
38
|
-
import static android.view.KeyEvent.KEYCODE_MEDIA_FAST_FORWARD;
|
|
39
|
-
import static android.view.KeyEvent.KEYCODE_MEDIA_NEXT;
|
|
40
|
-
import static android.view.KeyEvent.KEYCODE_MEDIA_PLAY;
|
|
41
|
-
import static android.view.KeyEvent.KEYCODE_MEDIA_PREVIOUS;
|
|
42
|
-
import static android.view.KeyEvent.KEYCODE_MEDIA_REWIND;
|
|
43
|
-
import static android.view.KeyEvent.KEYCODE_MEDIA_STOP;
|
|
44
|
-
import static androidx.core.app.NotificationCompat.PRIORITY_MIN;
|
|
45
|
-
import static com.guichaguri.trackplayer.service.Utils.bundleToJson;
|
|
46
|
-
import static com.guichaguri.trackplayer.service.Utils.jsonStringToBundle;
|
|
47
|
-
|
|
48
|
-
import java.util.List;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* @author Guichaguri
|
|
52
|
-
*/
|
|
53
|
-
public class MusicService extends HeadlessJsMediaService {
|
|
54
|
-
|
|
55
|
-
MusicManager manager;
|
|
56
|
-
Handler handler;
|
|
57
|
-
private Boolean intentToStop = false;
|
|
58
|
-
|
|
59
|
-
@Nullable
|
|
60
|
-
@Override
|
|
61
|
-
protected HeadlessJsTaskConfig getTaskConfig(Intent intent) {
|
|
62
|
-
return new HeadlessJsTaskConfig("TrackPlayer", Arguments.createMap(), 0, true);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
@Override
|
|
66
|
-
public void onHeadlessJsTaskFinish(int taskId) {
|
|
67
|
-
// Overridden to prevent the service from being terminated
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
public void emit(String event, Bundle data) {
|
|
71
|
-
Intent intent = new Intent(Utils.EVENT_INTENT);
|
|
72
|
-
|
|
73
|
-
intent.putExtra("event", event);
|
|
74
|
-
if(data != null) intent.putExtra("data", data);
|
|
75
|
-
|
|
76
|
-
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
public void destroy(boolean intentToStop) {
|
|
80
|
-
if(handler != null) {
|
|
81
|
-
handler.removeMessages(0);
|
|
82
|
-
handler = null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if(manager != null) {
|
|
86
|
-
manager.destroy(intentToStop);
|
|
87
|
-
manager = null;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
private void onStartForeground() {
|
|
92
|
-
boolean serviceForeground = false;
|
|
93
|
-
|
|
94
|
-
if(manager != null) {
|
|
95
|
-
// The session is only active when the service is on foreground
|
|
96
|
-
serviceForeground = manager.getMetadata().getSession().isActive();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if(!serviceForeground) {
|
|
100
|
-
ReactInstanceManager reactInstanceManager = getReactNativeHost().getReactInstanceManager();
|
|
101
|
-
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
|
|
102
|
-
|
|
103
|
-
// Checks whether there is a React activity
|
|
104
|
-
if(reactContext == null || !reactContext.hasCurrentActivity()) {
|
|
105
|
-
String channel = Utils.getNotificationChannel((Context) this);
|
|
106
|
-
|
|
107
|
-
// Sets the service to foreground with an empty notification
|
|
108
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
109
|
-
startForeground(1, new NotificationCompat.Builder(this, channel).build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
|
110
|
-
}else{
|
|
111
|
-
startForeground(1, new NotificationCompat.Builder(this, channel).build());
|
|
112
|
-
}
|
|
113
|
-
// Stops the service right after
|
|
114
|
-
stopSelf();
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
@Nullable
|
|
120
|
-
@Override
|
|
121
|
-
public IBinder onBind(Intent intent) {
|
|
122
|
-
if(Utils.CONNECT_INTENT.equals(intent.getAction())) {
|
|
123
|
-
return new MusicBinder(this, manager);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return super.onBind(intent);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
@androidx.annotation.Nullable
|
|
130
|
-
@Override
|
|
131
|
-
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @androidx.annotation.Nullable Bundle rootHints) {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
@Override
|
|
136
|
-
public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
@Override
|
|
141
|
-
public void onCreate() {
|
|
142
|
-
|
|
143
|
-
super.onCreate();
|
|
144
|
-
|
|
145
|
-
//onStartForeground();
|
|
146
|
-
|
|
147
|
-
if (manager == null)
|
|
148
|
-
manager = new MusicManager(this);
|
|
149
|
-
|
|
150
|
-
if(handler == null)
|
|
151
|
-
handler = new Handler();
|
|
152
|
-
|
|
153
|
-
String channel = Utils.getNotificationChannel((Context) this);
|
|
154
|
-
|
|
155
|
-
// Sets the service to foreground with an empty notification
|
|
156
|
-
try {
|
|
157
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
158
|
-
startForeground(1, new NotificationCompat.Builder(this, channel).build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
|
159
|
-
} else {
|
|
160
|
-
startForeground(1, new NotificationCompat.Builder(this, channel).build());
|
|
161
|
-
}
|
|
162
|
-
}catch(Exception ex){}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
@Override
|
|
166
|
-
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
167
|
-
if(intent != null && Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
|
|
168
|
-
onStartForeground();
|
|
169
|
-
|
|
170
|
-
if (Build.VERSION.SDK_INT >= 33) {
|
|
171
|
-
try {
|
|
172
|
-
startAndStopEmptyNotificationToAvoidANR();
|
|
173
|
-
} catch(Exception ex){}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
KeyEvent intentExtra = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
|
|
177
|
-
if (intentExtra.getKeyCode() == KEYCODE_MEDIA_STOP) {
|
|
178
|
-
intentToStop = true;
|
|
179
|
-
startServiceOreoAndAbove();
|
|
180
|
-
stopSelf();
|
|
181
|
-
} else {
|
|
182
|
-
intentToStop = false;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (manager != null && manager.getMetadata().getSession() != null) {
|
|
186
|
-
MediaButtonReceiver.handleIntent(manager.getMetadata().getSession(), intent);
|
|
187
|
-
return START_NOT_STICKY;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (manager == null)
|
|
192
|
-
manager = new MusicManager(this);
|
|
193
|
-
|
|
194
|
-
if(handler == null)
|
|
195
|
-
handler = new Handler();
|
|
196
|
-
|
|
197
|
-
super.onStartCommand(intent, flags, startId);
|
|
198
|
-
return START_NOT_STICKY;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
public void startServiceOreoAndAbove(){
|
|
202
|
-
// Needed to prevent crash when dismissing notification
|
|
203
|
-
// https://stackoverflow.com/questions/47609261/bound-service-crash-with-context-startforegroundservice-did-not-then-call-ser?rq=1
|
|
204
|
-
if (Build.VERSION.SDK_INT >= 26) {
|
|
205
|
-
String CHANNEL_ID = Utils.NOTIFICATION_CHANNEL;
|
|
206
|
-
String CHANNEL_NAME = "Playback";
|
|
207
|
-
|
|
208
|
-
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT);
|
|
209
|
-
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
|
|
210
|
-
|
|
211
|
-
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
|
|
212
|
-
.setCategory(Notification.CATEGORY_SERVICE).setSmallIcon(R.drawable.ic_logo).setPriority(PRIORITY_MIN).build();
|
|
213
|
-
|
|
214
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
215
|
-
startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
|
216
|
-
}else{
|
|
217
|
-
startForeground(1, notification);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Workaround for the "Context.startForegroundService() did not then call Service.startForeground()"
|
|
224
|
-
* within 5s" ANR and crash by creating an empty notification and stopping it right after. For more
|
|
225
|
-
* information see https://github.com/doublesymmetry/react-native-track-player/issues/1666
|
|
226
|
-
*/
|
|
227
|
-
private void startAndStopEmptyNotificationToAvoidANR() {
|
|
228
|
-
NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
|
|
229
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
230
|
-
notificationManager.createNotificationChannel(
|
|
231
|
-
new NotificationChannel("Playback", "Playback", NotificationManager.IMPORTANCE_LOW)
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, "Playback")
|
|
236
|
-
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
237
|
-
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
|
238
|
-
.setSmallIcon(R.drawable.ic_logo);
|
|
239
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
240
|
-
notificationBuilder.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE);
|
|
241
|
-
}
|
|
242
|
-
android.app.Notification notification = notificationBuilder.build();
|
|
243
|
-
startForeground(1, notification);
|
|
244
|
-
stopForeground(true);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
@Override
|
|
248
|
-
public void onDestroy() {
|
|
249
|
-
super.onDestroy();
|
|
250
|
-
if(manager != null) {
|
|
251
|
-
manager.destroy(true);
|
|
252
|
-
manager = null;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
@Override
|
|
257
|
-
public void onTaskRemoved(Intent rootIntent) {
|
|
258
|
-
super.onTaskRemoved(rootIntent);
|
|
259
|
-
|
|
260
|
-
if (manager == null || manager.shouldStopWithApp()) {
|
|
261
|
-
destroy(true);
|
|
262
|
-
stopSelf();
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
266
|
-
stopForeground(STOP_FOREGROUND_REMOVE);
|
|
267
|
-
} else {
|
|
268
|
-
stopForeground(true);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|