Import miniabs 1.0 source
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
4fe9eed870
78 files changed
+5800
No files matched your search
@@ -0,0 +1,762 @@
|
||||
package com.miniabs;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
import android.os.SystemClock;
|
||||
|
||||
/**
|
||||
* Plays one episode at a time, keeps the Audiobookshelf playback session in
|
||||
* sync, and falls back to saved files and local progress when offline.
|
||||
*/
|
||||
public class PlayerService extends Service implements MediaPlayer.OnPreparedListener,
|
||||
MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener, MediaPlayer.OnBufferingUpdateListener,
|
||||
AudioManager.OnAudioFocusChangeListener {
|
||||
|
||||
static final String A_PLAY = "com.miniabs.PLAY";
|
||||
static final String A_TOGGLE = "com.miniabs.TOGGLE";
|
||||
static final String A_PAUSE = "com.miniabs.PAUSE";
|
||||
static final String A_BACK = "com.miniabs.BACK";
|
||||
static final String A_FWD = "com.miniabs.FWD";
|
||||
static final String A_STOP = "com.miniabs.STOP";
|
||||
|
||||
static final int IDLE = 0, LOADING = 1, PLAYING = 2, PAUSED = 3, ENDED = 4, ERROR = 5;
|
||||
|
||||
private static final int NOTIF_ID = 1;
|
||||
private static final long SYNC_EVERY = 15000;
|
||||
private static final long IDLE_STOP = 30 * 60 * 1000;
|
||||
|
||||
private static PlayerService instance;
|
||||
|
||||
static PlayerService get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/** Starts playing an episode (or resumes it if it's already loaded). */
|
||||
static void play(Context c, Episode e) {
|
||||
c.startService(new Intent(c, PlayerService.class).setAction(A_PLAY).putExtra("ep", e.toJson().toString()));
|
||||
}
|
||||
|
||||
static void send(Context c, String action) {
|
||||
c.startService(new Intent(c, PlayerService.class).setAction(action));
|
||||
}
|
||||
|
||||
private final Handler h = new Handler();
|
||||
private final ExecutorService net = Executors.newSingleThreadExecutor();
|
||||
|
||||
private AudioManager am;
|
||||
private WifiManager.WifiLock wifi;
|
||||
private ComponentName buttons;
|
||||
|
||||
private MediaPlayer mp;
|
||||
private boolean prepared;
|
||||
private Episode ep;
|
||||
private volatile String sessionId;
|
||||
private String contentUrl; // server-relative stream URL, re-tokenised on retry
|
||||
private boolean local; // playing a downloaded file
|
||||
private int state = IDLE;
|
||||
private String status = "";
|
||||
private int resumeMs; // where to start once prepared
|
||||
private boolean playOnPrepared;
|
||||
private int lastPosMs;
|
||||
private int buffered; // percent
|
||||
private int generation; // drops results of superseded async work
|
||||
private int retries;
|
||||
private long playingSince;
|
||||
|
||||
private long lastTick;
|
||||
private float listened; // seconds listened since the last sync
|
||||
private long lastSync;
|
||||
private volatile boolean syncing;
|
||||
private long pausedAt;
|
||||
|
||||
private long sleepAt; // elapsedRealtime when the sleep timer fires, 0 = off
|
||||
private boolean sleepAtEnd;
|
||||
private boolean pausedByFocus;
|
||||
private boolean focusHeld;
|
||||
|
||||
private final BroadcastReceiver noisy = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (state == PLAYING) pause(); // headphones unplugged
|
||||
}
|
||||
};
|
||||
|
||||
private final Runnable ticker = new Runnable() {
|
||||
public void run() {
|
||||
tick();
|
||||
h.postDelayed(this, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
// --------------------------------------------------------------- lifecycle
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
instance = this;
|
||||
am = (AudioManager) getSystemService(AUDIO_SERVICE);
|
||||
wifi = ((WifiManager) getSystemService(WIFI_SERVICE)).createWifiLock(WifiManager.WIFI_MODE_FULL, "MiniABS");
|
||||
wifi.setReferenceCounted(false);
|
||||
buttons = new ComponentName(this, MediaButtonReceiver.class);
|
||||
registerReceiver(noisy, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
||||
h.post(ticker);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
h.removeCallbacks(ticker);
|
||||
if (ep != null && prepared && state != ENDED) syncNow(true);
|
||||
releasePlayer();
|
||||
net.shutdown();
|
||||
try {
|
||||
unregisterReceiver(noisy);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
am.unregisterMediaButtonEventReceiver(buttons);
|
||||
dropFocus();
|
||||
wifi.release();
|
||||
if (instance == this) instance = null;
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (intent == null || intent.getAction() == null) return START_NOT_STICKY;
|
||||
String a = intent.getAction();
|
||||
if (A_PLAY.equals(a)) {
|
||||
try {
|
||||
play(Episode.fromStored(new JSONObject(intent.getStringExtra("ep"))));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
} else if (A_TOGGLE.equals(a)) {
|
||||
toggle();
|
||||
} else if (A_PAUSE.equals(a)) {
|
||||
if (state == PLAYING) pause();
|
||||
} else if (A_BACK.equals(a)) {
|
||||
skip(-Prefs.get(this).skipBack());
|
||||
} else if (A_FWD.equals(a)) {
|
||||
skip(Prefs.get(this).skipFwd());
|
||||
} else if (A_STOP.equals(a)) {
|
||||
stop();
|
||||
}
|
||||
if (ep == null) stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- public state
|
||||
|
||||
Episode episode() { return ep; }
|
||||
int state() { return state; }
|
||||
String status() { return status; }
|
||||
boolean isLocal() { return local; }
|
||||
int bufferedPercent() { return local ? 100 : buffered; }
|
||||
|
||||
boolean isPlaying() {
|
||||
return state == PLAYING;
|
||||
}
|
||||
|
||||
int positionMs() {
|
||||
if (mp != null && prepared) {
|
||||
try {
|
||||
return mp.getCurrentPosition();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
return state == ENDED ? durationMs() : resumeMs > 0 ? resumeMs : lastPosMs;
|
||||
}
|
||||
|
||||
int durationMs() {
|
||||
if (ep != null && ep.duration > 0) return (int) (ep.duration * 1000);
|
||||
if (mp != null && prepared) {
|
||||
try {
|
||||
return mp.getDuration();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Milliseconds until the sleep timer fires; 0 = off, -1 = end of episode. */
|
||||
long sleepLeftMs() {
|
||||
if (sleepAtEnd) return -1;
|
||||
if (sleepAt == 0) return 0;
|
||||
return Math.max(0, sleepAt - SystemClock.elapsedRealtime());
|
||||
}
|
||||
|
||||
void setSleep(int minutes) {
|
||||
sleepAtEnd = minutes < 0;
|
||||
sleepAt = minutes > 0 ? SystemClock.elapsedRealtime() + minutes * 60000L : 0;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- controls
|
||||
|
||||
void play(Episode e) {
|
||||
if (ep != null && ep.id.equals(e.id) && mp != null && state != ERROR && state != ENDED) {
|
||||
if (state == PAUSED) resume();
|
||||
return;
|
||||
}
|
||||
if (ep != null && prepared && state != ENDED) syncNow(true);
|
||||
releasePlayer();
|
||||
ep = e;
|
||||
sessionId = null;
|
||||
contentUrl = null;
|
||||
retries = 0;
|
||||
lastPosMs = 0;
|
||||
resumeMs = 0;
|
||||
Prefs.get(this).setLastEpisode(e);
|
||||
open(e, -1, true);
|
||||
}
|
||||
|
||||
void toggle() {
|
||||
if (ep == null) {
|
||||
Episode last = Prefs.get(this).lastEpisode();
|
||||
if (last != null) play(last);
|
||||
return;
|
||||
}
|
||||
if (state == PLAYING) pause();
|
||||
else if (state == LOADING) playOnPrepared = !playOnPrepared;
|
||||
else resume();
|
||||
}
|
||||
|
||||
void pause() {
|
||||
if (mp == null || !prepared) {
|
||||
playOnPrepared = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mp.pause();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
lastPosMs = positionMs();
|
||||
state = PAUSED;
|
||||
status = "Paused";
|
||||
pausedAt = SystemClock.elapsedRealtime();
|
||||
syncNow(false);
|
||||
wifi.release();
|
||||
showNotification(false);
|
||||
}
|
||||
|
||||
void resume() {
|
||||
if (ep == null) return;
|
||||
if (state == ENDED) { // listen again from the start, in a fresh session
|
||||
App.setProgress(ep.id, 0, ep.duration, false);
|
||||
open(ep, 0, true);
|
||||
return;
|
||||
}
|
||||
if (mp == null || state == ERROR) {
|
||||
open(ep, lastPosMs / 1000.0, true);
|
||||
return;
|
||||
}
|
||||
if (prepared) start();
|
||||
else playOnPrepared = true;
|
||||
}
|
||||
|
||||
void seekTo(int ms) {
|
||||
int dur = durationMs();
|
||||
if (dur > 0 && ms > dur - 1000) ms = dur - 1000;
|
||||
if (ms < 0) ms = 0;
|
||||
if (mp != null && prepared) {
|
||||
try {
|
||||
mp.seekTo(ms);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
lastPosMs = ms;
|
||||
if (state == ENDED) state = PAUSED;
|
||||
} else {
|
||||
resumeMs = ms;
|
||||
lastPosMs = ms;
|
||||
}
|
||||
}
|
||||
|
||||
void skip(int seconds) {
|
||||
if (ep == null) return;
|
||||
seekTo(positionMs() + seconds * 1000);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (ep != null && prepared && state != ENDED) syncNow(true);
|
||||
releasePlayer();
|
||||
state = IDLE;
|
||||
status = "";
|
||||
am.unregisterMediaButtonEventReceiver(buttons);
|
||||
dropFocus();
|
||||
wifi.release();
|
||||
stopForeground(true);
|
||||
((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).cancel(NOTIF_ID);
|
||||
ep = null;
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
/** Marks the current episode as played on the server. */
|
||||
void markFinished() {
|
||||
if (ep == null) return;
|
||||
final Episode e = ep;
|
||||
final double dur = durationMs() / 1000.0;
|
||||
App.setProgress(e.id, dur, dur, true);
|
||||
final String sid = sessionId;
|
||||
sessionId = null;
|
||||
net.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
if (sid != null) Api.closeSession(PlayerService.this, sid, dur, 0, dur);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
try {
|
||||
Api.setProgress(PlayerService.this, e.itemId, e.id, dur, dur, Boolean.TRUE);
|
||||
App.forgetLocal(PlayerService.this, e.id);
|
||||
} catch (Exception ex) {
|
||||
App.saveLocal(PlayerService.this, e, dur, dur, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** "Mark as played" from the player screen: stop here and tell the server. */
|
||||
void finishAndStop() {
|
||||
if (ep == null) return;
|
||||
int dur = durationMs();
|
||||
markFinished();
|
||||
releasePlayer();
|
||||
state = ENDED;
|
||||
status = "Marked as played";
|
||||
lastPosMs = dur;
|
||||
pausedAt = SystemClock.elapsedRealtime();
|
||||
wifi.release();
|
||||
showNotification(false);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- opening
|
||||
|
||||
private static final class Source {
|
||||
String url, sessionId, contentUrl, error;
|
||||
boolean local, offline;
|
||||
double start;
|
||||
}
|
||||
|
||||
/** Opens a session and the audio. startSec < 0 means "resume where the server says". */
|
||||
private void open(final Episode e, final double startSec, boolean playNow) {
|
||||
releasePlayer();
|
||||
state = LOADING;
|
||||
status = "Connecting…";
|
||||
playOnPrepared = playNow;
|
||||
showNotification(true);
|
||||
final int gen = ++generation;
|
||||
final Context ctx = getApplicationContext();
|
||||
net.execute(new Runnable() {
|
||||
public void run() {
|
||||
final Source s = findSource(ctx, e, startSec);
|
||||
h.post(new Runnable() {
|
||||
public void run() {
|
||||
if (gen == generation) startSource(s);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Source findSource(Context c, Episode e, double startSec) {
|
||||
Source s = new Source();
|
||||
File f = Downloads.audio(c, e);
|
||||
double localPos = App.localPosition(c, e.id);
|
||||
boolean localNewer = App.localPending(c, e.id);
|
||||
try {
|
||||
if (f != null && !Util.online(c)) throw new java.io.IOException("offline");
|
||||
Api.ensureFresh(c, 1800);
|
||||
JSONObject sess = Api.startPlayback(c, e);
|
||||
s.sessionId = Util.str(sess, "id");
|
||||
s.start = sess.optDouble("currentTime", 0);
|
||||
if (localNewer && localPos >= 0) s.start = localPos;
|
||||
if (f != null) {
|
||||
s.url = f.getAbsolutePath();
|
||||
s.local = true;
|
||||
} else {
|
||||
JSONArray tracks = sess.optJSONArray("audioTracks");
|
||||
JSONObject t = tracks == null ? null : tracks.optJSONObject(0);
|
||||
String cu = t == null ? "" : Util.str(t, "contentUrl");
|
||||
if (cu.length() == 0) {
|
||||
s.error = "The server sent no audio for this episode.";
|
||||
} else if (cu.contains(".m3u8")) {
|
||||
s.error = "This episode needs transcoding, which Android 2.x can't play. Try downloading it.";
|
||||
} else {
|
||||
s.contentUrl = cu;
|
||||
s.url = Api.withToken(c, Api.absolute(c, cu));
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
if (f != null) {
|
||||
s.url = f.getAbsolutePath();
|
||||
s.local = true;
|
||||
s.offline = true;
|
||||
s.start = Math.max(0, localPos);
|
||||
} else {
|
||||
s.error = Util.describe(ex);
|
||||
}
|
||||
}
|
||||
if (startSec >= 0) s.start = startSec;
|
||||
double[] p = App.progress(e.id);
|
||||
if (startSec < 0 && p != null && p[2] > 0) s.start = 0; // finished before: start over
|
||||
if (e.duration > 0 && s.start > e.duration - 5) s.start = 0;
|
||||
return s;
|
||||
}
|
||||
|
||||
private void startSource(Source s) {
|
||||
if (s.error != null) {
|
||||
fail(s.error);
|
||||
return;
|
||||
}
|
||||
sessionId = s.sessionId;
|
||||
contentUrl = s.contentUrl;
|
||||
local = s.local;
|
||||
resumeMs = (int) (s.start * 1000);
|
||||
lastPosMs = resumeMs;
|
||||
prepare(s.url, s.offline ? "Offline · progress will sync later" : null);
|
||||
}
|
||||
|
||||
private void prepare(String url, String note) {
|
||||
releasePlayer();
|
||||
mp = new MediaPlayer();
|
||||
mp.setAudioStreamType(AudioManager.STREAM_MUSIC);
|
||||
mp.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
|
||||
mp.setOnPreparedListener(this);
|
||||
mp.setOnCompletionListener(this);
|
||||
mp.setOnErrorListener(this);
|
||||
mp.setOnBufferingUpdateListener(this);
|
||||
try {
|
||||
if (local) {
|
||||
FileInputStream in = new FileInputStream(url);
|
||||
try {
|
||||
mp.setDataSource(in.getFD());
|
||||
} finally {
|
||||
in.close();
|
||||
}
|
||||
} else {
|
||||
mp.setDataSource(url);
|
||||
wifi.acquire();
|
||||
}
|
||||
state = LOADING;
|
||||
status = note != null ? note : local ? "Opening saved file…" : "Buffering…";
|
||||
mp.prepareAsync();
|
||||
} catch (Exception ex) {
|
||||
fail("Can't open the audio: " + Util.describe(ex));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepared(MediaPlayer p) {
|
||||
if (p != mp) return;
|
||||
prepared = true;
|
||||
int dur = durationMs();
|
||||
if (resumeMs > 1000 && (dur <= 0 || resumeMs < dur - 2000)) {
|
||||
try {
|
||||
mp.seekTo(resumeMs);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
resumeMs = 0;
|
||||
if (playOnPrepared) {
|
||||
start();
|
||||
} else {
|
||||
state = PAUSED;
|
||||
status = "Paused";
|
||||
showNotification(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void start() {
|
||||
if (mp == null) return;
|
||||
if (!focusHeld) {
|
||||
int r = am.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
|
||||
focusHeld = r == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
|
||||
}
|
||||
am.registerMediaButtonEventReceiver(buttons);
|
||||
try {
|
||||
mp.setVolume(1f, 1f);
|
||||
mp.start();
|
||||
} catch (Exception ex) {
|
||||
fail("Playback failed: " + Util.describe(ex));
|
||||
return;
|
||||
}
|
||||
if (!local) wifi.acquire();
|
||||
state = PLAYING;
|
||||
status = local ? (sessionId == null ? "Saved file · offline" : "Saved file") : "Streaming";
|
||||
playingSince = SystemClock.elapsedRealtime();
|
||||
lastTick = playingSince;
|
||||
if (lastSync == 0) lastSync = playingSince;
|
||||
showNotification(true);
|
||||
}
|
||||
|
||||
private void fail(String msg) {
|
||||
releasePlayer();
|
||||
state = ERROR;
|
||||
status = msg;
|
||||
pausedAt = SystemClock.elapsedRealtime();
|
||||
wifi.release();
|
||||
showNotification(false);
|
||||
}
|
||||
|
||||
private void releasePlayer() {
|
||||
if (mp != null) {
|
||||
if (prepared) lastPosMs = positionMs();
|
||||
try {
|
||||
mp.reset();
|
||||
mp.release();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
mp = null;
|
||||
prepared = false;
|
||||
buffered = 0;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- player callbacks
|
||||
|
||||
@Override
|
||||
public void onBufferingUpdate(MediaPlayer p, int percent) {
|
||||
if (p == mp) buffered = percent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompletion(MediaPlayer p) {
|
||||
if (p != mp) return;
|
||||
int pos = positionMs(), dur = durationMs();
|
||||
// Old Android's streamer "completes" when the connection drops. Treat that as an error.
|
||||
if (!local && dur > 0 && pos < dur - 30000) {
|
||||
lastPosMs = pos;
|
||||
retryStream("Connection lost, reconnecting…");
|
||||
return;
|
||||
}
|
||||
state = ENDED;
|
||||
status = "Finished";
|
||||
lastPosMs = dur;
|
||||
pausedAt = SystemClock.elapsedRealtime();
|
||||
wifi.release();
|
||||
if (sleepAtEnd) sleepAtEnd = false;
|
||||
markFinished();
|
||||
showNotification(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onError(MediaPlayer p, int what, int extra) {
|
||||
if (p != mp) return true;
|
||||
if (prepared) lastPosMs = positionMs();
|
||||
if (!local && contentUrl != null) {
|
||||
retryStream("Connection lost, reconnecting…");
|
||||
} else {
|
||||
fail("Playback error (" + what + "/" + extra + ").");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Re-opens the stream at the last position with a fresh token. Gives up after three tries. */
|
||||
private void retryStream(final String msg) {
|
||||
if (retries >= 3 || contentUrl == null) {
|
||||
fail("Lost connection to the server. Press play to try again.");
|
||||
retries = 0;
|
||||
return;
|
||||
}
|
||||
retries++;
|
||||
final boolean wasPlaying = state == PLAYING || playOnPrepared;
|
||||
final int pos = lastPosMs;
|
||||
final String cu = contentUrl;
|
||||
releasePlayer();
|
||||
state = LOADING;
|
||||
status = msg;
|
||||
final int gen = ++generation;
|
||||
final Context ctx = getApplicationContext();
|
||||
h.postDelayed(new Runnable() {
|
||||
public void run() {
|
||||
if (gen != generation) return;
|
||||
net.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
if (Prefs.get(ctx).canRefresh()) Api.refresh(ctx, true);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
h.post(new Runnable() {
|
||||
public void run() {
|
||||
if (gen != generation) return;
|
||||
resumeMs = pos;
|
||||
playOnPrepared = wasPlaying;
|
||||
prepare(Api.withToken(ctx, Api.absolute(ctx, cu)), msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 1500L * retries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioFocusChange(int change) {
|
||||
switch (change) {
|
||||
case AudioManager.AUDIOFOCUS_LOSS:
|
||||
focusHeld = false;
|
||||
pausedByFocus = false;
|
||||
if (state == PLAYING) pause();
|
||||
break;
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
||||
if (state == PLAYING) {
|
||||
pause();
|
||||
pausedByFocus = true;
|
||||
}
|
||||
break;
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
|
||||
if (mp != null && state == PLAYING) mp.setVolume(0.2f, 0.2f);
|
||||
break;
|
||||
case AudioManager.AUDIOFOCUS_GAIN:
|
||||
focusHeld = true;
|
||||
if (mp != null) mp.setVolume(1f, 1f);
|
||||
if (pausedByFocus) {
|
||||
pausedByFocus = false;
|
||||
if (state == PAUSED) start();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void dropFocus() {
|
||||
am.abandonAudioFocus(this);
|
||||
focusHeld = false;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- sync
|
||||
|
||||
private void tick() {
|
||||
if (ep == null) return;
|
||||
long now = SystemClock.elapsedRealtime();
|
||||
if (state == PLAYING) {
|
||||
float d = (now - lastTick) / 1000f;
|
||||
if (d > 0 && d < 5) listened += d;
|
||||
lastTick = now;
|
||||
lastPosMs = positionMs();
|
||||
double dur = durationMs() / 1000.0;
|
||||
App.setProgress(ep.id, lastPosMs / 1000.0, dur, false);
|
||||
if (now - lastSync > SYNC_EVERY) syncNow(false);
|
||||
if (now - playingSince > 60000) retries = 0;
|
||||
if (sleepAt > 0 && now >= sleepAt) {
|
||||
sleepAt = 0;
|
||||
pause();
|
||||
status = "Sleep timer: paused";
|
||||
}
|
||||
} else if (state == PAUSED || state == ENDED || state == ERROR) {
|
||||
// Free the phone's memory if left paused for a long time.
|
||||
if (pausedAt > 0 && now - pausedAt > IDLE_STOP) {
|
||||
pausedAt = 0;
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Sends the position to the server (or keeps it on the phone when offline). */
|
||||
private void syncNow(final boolean close) {
|
||||
if (ep == null) return;
|
||||
if (syncing && !close) return;
|
||||
final Episode e = ep;
|
||||
final double cur = (prepared ? positionMs() : lastPosMs) / 1000.0;
|
||||
final double dur = durationMs() / 1000.0;
|
||||
final float tl = listened;
|
||||
final String sid = sessionId;
|
||||
final Context c = getApplicationContext();
|
||||
listened = 0;
|
||||
lastSync = SystemClock.elapsedRealtime();
|
||||
if (close) sessionId = null;
|
||||
if (cur <= 0 && tl <= 0) return;
|
||||
syncing = true;
|
||||
final boolean saved = Downloads.has(c, e.id);
|
||||
Runnable job = new Runnable() {
|
||||
public void run() {
|
||||
boolean ok = false;
|
||||
try {
|
||||
if (sid != null) {
|
||||
try {
|
||||
if (close) Api.closeSession(c, sid, cur, tl, dur);
|
||||
else Api.syncSession(c, sid, cur, tl, dur);
|
||||
ok = true;
|
||||
} catch (Api.ApiException ex) {
|
||||
if (ex.code == 404) {
|
||||
// Session expired on the server (e.g. it restarted): save progress directly.
|
||||
h.post(new Runnable() {
|
||||
public void run() {
|
||||
if (sid.equals(sessionId)) sessionId = null;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!ok && Util.online(c)) {
|
||||
Api.setProgress(c, e.itemId, e.id, cur, dur, null);
|
||||
ok = true;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
ok = false;
|
||||
}
|
||||
if (!ok) App.saveLocal(c, e, cur, dur, true);
|
||||
else if (saved) App.saveLocal(c, e, cur, dur, false);
|
||||
else App.forgetLocal(c, e.id);
|
||||
syncing = false;
|
||||
}
|
||||
};
|
||||
try {
|
||||
net.execute(job);
|
||||
} catch (Exception ex) {
|
||||
// executor already shut down (service ending): keep the position on the phone
|
||||
App.saveLocal(c, e, cur, dur, true);
|
||||
syncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- notification
|
||||
|
||||
private void showNotification(boolean foreground) {
|
||||
if (ep == null) return;
|
||||
Notification n = new Notification(R.drawable.ic_stat, null, 0);
|
||||
n.flags |= foreground ? Notification.FLAG_ONGOING_EVENT : 0;
|
||||
Intent i = new Intent(this, PlayerActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
PendingIntent pi = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
String what;
|
||||
switch (state) {
|
||||
case PLAYING: what = "Playing"; break;
|
||||
case LOADING: what = "Loading…"; break;
|
||||
case ENDED: what = "Finished"; break;
|
||||
case ERROR: what = "Stopped: tap to retry"; break;
|
||||
default: what = "Paused"; break;
|
||||
}
|
||||
n.setLatestEventInfo(this, ep.title, Util.join(what, ep.show), pi);
|
||||
if (foreground) {
|
||||
startForeground(NOTIF_ID, n);
|
||||
} else {
|
||||
stopForeground(false);
|
||||
((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).notify(NOTIF_ID, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user