Private
Public Access
- Transcoding: send client identities Plex has a profile for (unknown ones get 400 "unable to find a matching profile"), with a fresh session id per variant - Stream proxy: fetch transcodes once into a temp file and serve every player request from it, so reconnects no longer restart the transcode (503s, tracks cut short); original files still pass Range requests through - Sign-in: remember the pending link code so it survives Minamp being closed while the code is entered elsewhere; add "I've entered it - check now" - Diagnostics: in-app connection log (DiagLog) with a streaming test and save to SD card; tokens are never logged Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
574 lines
25 KiB
Java
574 lines
25 KiB
Java
package com.minamp;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.concurrent.ExecutorService;
|
|
|
|
import android.app.Activity;
|
|
import android.app.AlertDialog;
|
|
import android.content.DialogInterface;
|
|
import android.content.Intent;
|
|
import android.view.Menu;
|
|
import android.view.MenuItem;
|
|
import android.view.View;
|
|
import android.widget.ImageButton;
|
|
import android.widget.ImageView;
|
|
import android.widget.ProgressBar;
|
|
import android.widget.TextView;
|
|
import android.widget.Toast;
|
|
|
|
import com.minamp.plex.Item;
|
|
import com.minamp.plex.Library;
|
|
import com.minamp.plex.Server;
|
|
|
|
/** Shared bits: background tasks, mini player, item actions, settings menu. */
|
|
public abstract class BaseActivity extends Activity implements PlayerService.Listener, PlayQueue.Listener {
|
|
|
|
private static final ExecutorService POOL = com.minamp.net.Threads.pool("task", 2);
|
|
|
|
protected App app;
|
|
protected boolean destroyed;
|
|
protected boolean visible;
|
|
private View mini;
|
|
private ImageView miniArt;
|
|
private TextView miniTitle, miniSub;
|
|
private ImageButton miniPlay;
|
|
private ProgressBar miniProgress;
|
|
private Glyph miniPlayGlyph;
|
|
private String miniThumb;
|
|
|
|
// ------------------------------------------------------------------ tasks
|
|
|
|
public abstract static class Task<T> {
|
|
public abstract T work() throws Exception;
|
|
public void done(T result) {}
|
|
public void fail(Exception e) {}
|
|
}
|
|
|
|
protected <T> void run(final Task<T> t) {
|
|
POOL.execute(new Runnable() {
|
|
public void run() {
|
|
try {
|
|
final T r = t.work();
|
|
app.handler.post(new Runnable() { public void run() { if (!destroyed) t.done(r); } });
|
|
} catch (final Exception e) {
|
|
app.handler.post(new Runnable() { public void run() { if (!destroyed) t.fail(e); } });
|
|
} catch (final Throwable e) {
|
|
// Errors (out of memory, stack overflow, a class the old VM can't load) must not kill the app
|
|
if (e instanceof OutOfMemoryError) app.images.clearMemory();
|
|
App.logError("task", e);
|
|
final Exception wrapped = new Exception(com.minamp.net.Threads.describe(e));
|
|
app.handler.post(new Runnable() { public void run() { if (!destroyed) t.fail(wrapped); } });
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
protected void toast(String s) { Toast.makeText(this, s, s != null && s.length() > 40 ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show(); }
|
|
|
|
protected static String msg(Exception e) { return PlayerService.message(e); }
|
|
|
|
// ------------------------------------------------------------------ lifecycle
|
|
|
|
@Override
|
|
protected void onCreate(android.os.Bundle b) {
|
|
super.onCreate(b);
|
|
app = App.get();
|
|
}
|
|
|
|
/** If the last run hit an error, show it so it can be reported (also saved as Minamp-error.txt on the SD card). */
|
|
protected void showCrashReportIfAny() {
|
|
final String report = app.takeCrashReport();
|
|
if (report == null) return;
|
|
android.widget.ScrollView sv = new android.widget.ScrollView(this);
|
|
TextView tv = new TextView(this);
|
|
tv.setText(report);
|
|
tv.setTextSize(10);
|
|
tv.setTextColor(0xFFDDDDDD);
|
|
int p = Ui.dp(this, 8);
|
|
tv.setPadding(p, p, p, p);
|
|
sv.addView(tv);
|
|
new AlertDialog.Builder(this)
|
|
.setTitle("Minamp hit an error last time")
|
|
.setView(sv)
|
|
.setPositiveButton("OK", null)
|
|
.show();
|
|
}
|
|
|
|
@Override
|
|
protected void onResume() {
|
|
super.onResume();
|
|
visible = true;
|
|
PlayerService.addListener(this);
|
|
PlayQueue.get().addListener(this);
|
|
bindMini();
|
|
app.handler.post(tick);
|
|
}
|
|
|
|
@Override
|
|
protected void onPause() {
|
|
super.onPause();
|
|
visible = false;
|
|
PlayerService.removeListener(this);
|
|
PlayQueue.get().removeListener(this);
|
|
app.handler.removeCallbacks(tick);
|
|
}
|
|
|
|
@Override
|
|
protected void onDestroy() {
|
|
destroyed = true;
|
|
super.onDestroy();
|
|
}
|
|
|
|
private final Runnable tick = new Runnable() {
|
|
public void run() {
|
|
if (!visible) return;
|
|
onTick();
|
|
app.handler.postDelayed(this, 500);
|
|
}
|
|
};
|
|
|
|
protected void onTick() { updateMiniProgress(); }
|
|
|
|
public void onPlayerChanged() { updateMini(); }
|
|
|
|
public void onQueueChanged() { updateMini(); }
|
|
|
|
// ------------------------------------------------------------------ top bar
|
|
|
|
protected void setupTopBar(String title, String sub, boolean back) {
|
|
TextView t = (TextView) findViewById(R.id.bar_title);
|
|
if (t != null) t.setText(title);
|
|
setBarSub(sub);
|
|
ImageButton b = (ImageButton) findViewById(R.id.btn_back);
|
|
if (b != null && back) {
|
|
b.setVisibility(View.VISIBLE);
|
|
Ui.icon(b, Glyph.BACK, getResources().getColor(R.color.text));
|
|
b.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { finish(); } });
|
|
}
|
|
}
|
|
|
|
protected void setBarSub(String sub) {
|
|
TextView s = (TextView) findViewById(R.id.bar_sub);
|
|
if (s == null) return;
|
|
s.setVisibility(sub == null || sub.length() == 0 ? View.GONE : View.VISIBLE);
|
|
s.setText(sub);
|
|
}
|
|
|
|
protected ImageButton barAction(int id, int glyph, View.OnClickListener l) {
|
|
ImageButton b = (ImageButton) findViewById(id);
|
|
b.setVisibility(View.VISIBLE);
|
|
Ui.icon(b, glyph, getResources().getColor(R.color.text));
|
|
b.setOnClickListener(l);
|
|
return b;
|
|
}
|
|
|
|
// ------------------------------------------------------------------ status overlay
|
|
|
|
protected void showStatus(boolean spinning, String text, final Runnable retry) {
|
|
View box = findViewById(R.id.status_box);
|
|
if (box == null) return;
|
|
box.setVisibility(View.VISIBLE);
|
|
findViewById(R.id.status_spin).setVisibility(spinning ? View.VISIBLE : View.GONE);
|
|
((TextView) findViewById(R.id.status_text)).setText(text);
|
|
View r = findViewById(R.id.status_retry);
|
|
r.setVisibility(retry != null ? View.VISIBLE : View.GONE);
|
|
if (retry != null) r.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { retry.run(); } });
|
|
}
|
|
|
|
protected void hideStatus() {
|
|
View box = findViewById(R.id.status_box);
|
|
if (box != null) box.setVisibility(View.GONE);
|
|
}
|
|
|
|
// ------------------------------------------------------------------ mini player
|
|
|
|
private void bindMini() {
|
|
if (mini == null) {
|
|
mini = findViewById(R.id.mini_root);
|
|
if (mini == null) return;
|
|
miniArt = (ImageView) findViewById(R.id.mini_art);
|
|
miniTitle = (TextView) findViewById(R.id.mini_title);
|
|
miniSub = (TextView) findViewById(R.id.mini_sub);
|
|
miniPlay = (ImageButton) findViewById(R.id.mini_play);
|
|
miniProgress = (ProgressBar) findViewById(R.id.mini_progress);
|
|
miniPlayGlyph = Ui.glyph(this, Glyph.PLAY, getResources().getColor(R.color.text), 18);
|
|
miniPlayGlyph.scale(1f);
|
|
miniPlay.setImageDrawable(miniPlayGlyph);
|
|
ImageButton next = (ImageButton) findViewById(R.id.mini_next);
|
|
Ui.icon(next, Glyph.NEXT, getResources().getColor(R.color.text), 16);
|
|
miniPlay.setOnClickListener(new View.OnClickListener() {
|
|
public void onClick(View v) { PlayerService.send(BaseActivity.this, PlayerService.ACTION_TOGGLE); }
|
|
});
|
|
next.setOnClickListener(new View.OnClickListener() {
|
|
public void onClick(View v) { PlayerService.send(BaseActivity.this, PlayerService.ACTION_NEXT); }
|
|
});
|
|
findViewById(R.id.mini_body).setOnClickListener(new View.OnClickListener() {
|
|
public void onClick(View v) { startActivity(new Intent(BaseActivity.this, NowPlayingActivity.class)); }
|
|
});
|
|
}
|
|
updateMini();
|
|
}
|
|
|
|
private void updateMini() {
|
|
if (mini == null) return;
|
|
Item it = PlayQueue.get().current();
|
|
if (it == null) { mini.setVisibility(View.GONE); return; }
|
|
mini.setVisibility(View.VISIBLE);
|
|
miniTitle.setText(it.title);
|
|
String st = PlayerService.status();
|
|
miniSub.setText(PlayerService.state() == PlayerService.BUFFERING && st.length() > 0 ? st : it.artist);
|
|
miniPlayGlyph.setType(PlayerService.isPlaying() ? Glyph.PAUSE : Glyph.PLAY);
|
|
if (!it.thumb.equals(miniThumb)) {
|
|
miniThumb = it.thumb;
|
|
app.images.load(miniArt, it.thumb, Ui.dp(this, 40), R.drawable.art_placeholder);
|
|
}
|
|
updateMiniProgress();
|
|
}
|
|
|
|
private void updateMiniProgress() {
|
|
if (mini == null || miniProgress == null) return;
|
|
long d = PlayerService.duration();
|
|
miniProgress.setProgress(d > 0 ? (int) (PlayerService.position() * 1000 / d) : 0);
|
|
}
|
|
|
|
// ------------------------------------------------------------------ item actions
|
|
|
|
protected Library lib() { return app.library; }
|
|
|
|
/** Tap on an item in a list. `context` = the list it's in (tracks play from there). */
|
|
protected void open(Item it, List<Item> contextTracks, int trackIndex, String sourceTitle) {
|
|
switch (it.type) {
|
|
case Item.ARTIST:
|
|
case Item.ALBUM:
|
|
case Item.PLAYLIST:
|
|
startActivity(new Intent(this, BrowseActivity.class).putExtra(BrowseActivity.EXTRA_ITEM, it.serialize()));
|
|
break;
|
|
case Item.TRACK:
|
|
List<Item> q = contextTracks;
|
|
int idx = trackIndex;
|
|
if (q == null || q.isEmpty()) { q = new ArrayList<Item>(); q.add(it); idx = 0; }
|
|
playList(q, idx, false, sourceTitle);
|
|
break;
|
|
}
|
|
}
|
|
|
|
protected void playList(List<Item> tracks, int start, boolean shuffle, String title) {
|
|
if (tracks.isEmpty()) { toast("Nothing to play"); return; }
|
|
PlayQueue.get().set(tracks, start, shuffle, title);
|
|
PlayerService.playCurrent(this, 0);
|
|
}
|
|
|
|
/** Fetch the tracks of an album/artist/playlist, then play/queue them. */
|
|
protected void playContainer(final Item it, final boolean shuffle, final int how) {
|
|
final Library lib = lib();
|
|
if (lib == null) return;
|
|
toast(how == 0 ? (shuffle ? "Shuffling " : "Playing ") + it.title : "Adding " + it.title + "…");
|
|
run(new Task<List<Item>>() {
|
|
public List<Item> work() throws Exception { return lib.tracksOf(it); }
|
|
public void done(List<Item> tracks) {
|
|
if (tracks.isEmpty()) { toast("No tracks"); return; }
|
|
boolean wasEmpty = PlayQueue.get().size() == 0;
|
|
if (how == 1) { PlayQueue.get().playNext(tracks); toast("Playing next"); }
|
|
else if (how == 2) { PlayQueue.get().append(tracks); toast("Added to queue"); }
|
|
else { playList(tracks, shuffle ? -1 : 0, shuffle, it.title); return; }
|
|
if (wasEmpty) PlayerService.playCurrent(BaseActivity.this, 0);
|
|
}
|
|
public void fail(Exception e) { toast(msg(e)); }
|
|
});
|
|
}
|
|
|
|
protected void runRadio(final String title, final Task<List<Item>> fetch) {
|
|
toast("Starting " + title + "…");
|
|
run(new Task<List<Item>>() {
|
|
public List<Item> work() throws Exception { return fetch.work(); }
|
|
public void done(List<Item> tracks) {
|
|
if (tracks.isEmpty()) { toast("Nothing found for " + title); return; }
|
|
playList(tracks, 0, false, title);
|
|
}
|
|
public void fail(Exception e) { toast(msg(e)); }
|
|
});
|
|
}
|
|
|
|
/** Long-press menu for any item. */
|
|
protected void itemMenu(final Item it) {
|
|
final List<String> labels = new ArrayList<String>();
|
|
final List<Integer> actions = new ArrayList<Integer>();
|
|
if (it.type == Item.TRACK) {
|
|
labels.add("Play next"); actions.add(1);
|
|
labels.add("Add to queue"); actions.add(2);
|
|
if (it.albumKey.length() > 0) { labels.add("Go to album"); actions.add(5); }
|
|
if (it.artistKey.length() > 0) { labels.add("Go to artist"); actions.add(6); }
|
|
labels.add(it.loved() ? "Unlove" : "Love"); actions.add(7);
|
|
} else if (it.isContainer()) {
|
|
labels.add("Play"); actions.add(0);
|
|
labels.add(it.type == Item.ARTIST ? "Artist radio (shuffle)" : "Shuffle"); actions.add(3);
|
|
labels.add("Play next"); actions.add(1);
|
|
labels.add("Add to queue"); actions.add(2);
|
|
if (it.type == Item.ALBUM && it.artistKey.length() > 0) { labels.add("Go to artist"); actions.add(6); }
|
|
} else return;
|
|
new AlertDialog.Builder(this)
|
|
.setTitle(it.title)
|
|
.setItems(labels.toArray(new String[labels.size()]), new DialogInterface.OnClickListener() {
|
|
public void onClick(DialogInterface d, int which) { itemAction(it, actions.get(which).intValue()); }
|
|
})
|
|
.show();
|
|
}
|
|
|
|
protected void itemAction(final Item it, int action) {
|
|
switch (action) {
|
|
case 0: playContainer(it, false, 0); break;
|
|
case 3: playContainer(it, true, 0); break;
|
|
case 1:
|
|
case 2:
|
|
if (it.type == Item.TRACK) {
|
|
List<Item> one = new ArrayList<Item>();
|
|
one.add(it);
|
|
boolean wasEmpty = PlayQueue.get().size() == 0;
|
|
if (action == 1) PlayQueue.get().playNext(one); else PlayQueue.get().append(one);
|
|
if (wasEmpty) PlayerService.playCurrent(this, 0);
|
|
toast(action == 1 ? "Playing next" : "Added to queue");
|
|
} else {
|
|
playContainer(it, false, action);
|
|
}
|
|
break;
|
|
case 5: openKey(it.albumKey, Item.ALBUM, it.album); break;
|
|
case 6: openKey(it.artistKey, Item.ARTIST, it.type == Item.ALBUM ? it.artist : it.artist); break;
|
|
case 7: toggleLove(it, null); break;
|
|
}
|
|
}
|
|
|
|
protected void openKey(String ratingKey, int type, String title) {
|
|
Item i = new Item();
|
|
i.type = type;
|
|
i.ratingKey = ratingKey;
|
|
i.title = title;
|
|
startActivity(new Intent(this, BrowseActivity.class).putExtra(BrowseActivity.EXTRA_ITEM, i.serialize()));
|
|
}
|
|
|
|
protected void toggleLove(final Item it, final Runnable after) {
|
|
final Library lib = lib();
|
|
if (lib == null) return;
|
|
final boolean love = !it.loved();
|
|
final double old = it.userRating;
|
|
it.userRating = love ? 10 : -1;
|
|
Item cur = PlayQueue.get().current();
|
|
if (cur != null && cur != it && cur.ratingKey.equals(it.ratingKey)) cur.userRating = it.userRating;
|
|
if (after != null) after.run();
|
|
run(new Task<Object>() {
|
|
public Object work() throws Exception { lib.rate(it, love ? 10 : -1); return null; }
|
|
public void done(Object o) { toast(love ? "Loved" : "Removed from loved"); }
|
|
public void fail(Exception e) {
|
|
it.userRating = old;
|
|
if (after != null) after.run();
|
|
toast("Couldn't save rating: " + msg(e));
|
|
}
|
|
});
|
|
}
|
|
|
|
// ------------------------------------------------------------------ menu
|
|
|
|
@Override
|
|
public boolean onCreateOptionsMenu(Menu menu) {
|
|
menu.add(0, 1, 0, "Now playing");
|
|
menu.add(0, 2, 0, "Search");
|
|
menu.add(0, 3, 0, "Settings");
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean onOptionsItemSelected(MenuItem item) {
|
|
switch (item.getItemId()) {
|
|
case 1: startActivity(new Intent(this, NowPlayingActivity.class)); return true;
|
|
case 2: startActivity(new Intent(this, SearchActivity.class)); return true;
|
|
case 3: showSettings(); return true;
|
|
}
|
|
return super.onOptionsItemSelected(item);
|
|
}
|
|
|
|
protected void showSettings() {
|
|
Server s = app.server;
|
|
String conn = s == null || s.active() == null ? "not connected yet" : Server.kindLabel(s.active().kind)
|
|
+ (s.active().uri.startsWith("https") ? " (secure)" : " (unencrypted)");
|
|
final String[] rows = {
|
|
"Quality at home: " + qualityLabel(app.qualityHome()),
|
|
"Quality away / mobile data: " + qualityLabel(app.qualityAway()),
|
|
"Download tracks before playing: " + (app.downloadFirst() ? "On" : "Off"),
|
|
"Library: " + app.sectionTitle(),
|
|
"Server: " + (s == null ? "-" : s.name),
|
|
"Connection: " + conn,
|
|
"Connection log & streaming test",
|
|
"Sign out",
|
|
};
|
|
new AlertDialog.Builder(this)
|
|
.setTitle("Settings")
|
|
.setItems(rows, new DialogInterface.OnClickListener() {
|
|
public void onClick(DialogInterface d, int which) {
|
|
switch (which) {
|
|
case 0: pickQuality("qHome", app.qualityHome(), "Quality at home"); break;
|
|
case 1: pickQuality("qAway", app.qualityAway(), "Quality away / mobile"); break;
|
|
case 2:
|
|
app.prefs.edit().putBoolean("downloadFirst", !app.downloadFirst()).commit();
|
|
toast("Download before playing: " + (app.downloadFirst() ? "on" : "off"));
|
|
break;
|
|
case 3: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_LIBRARY)); break;
|
|
case 4: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_SERVER)); break;
|
|
case 5:
|
|
if (app.server != null) {
|
|
app.server.invalidate();
|
|
toast("Reconnecting…");
|
|
final Server sv = app.server;
|
|
run(new Task<Server.Conn>() {
|
|
public Server.Conn work() throws Exception { return sv.pick(); }
|
|
public void done(Server.Conn c) { toast("Connected: " + Server.kindLabel(c.kind)); onConnectionChanged(); }
|
|
public void fail(Exception e) { toast(msg(e)); }
|
|
});
|
|
}
|
|
break;
|
|
case 6: showDiagnostics(); break;
|
|
case 7: confirmSignOut(); break;
|
|
}
|
|
}
|
|
}).show();
|
|
}
|
|
|
|
protected void onConnectionChanged() {}
|
|
|
|
// ------------------------------------------------------------------ diagnostics
|
|
|
|
/** Connection log with "test streaming" and "save to SD card", for reporting problems. */
|
|
protected void showDiagnostics() {
|
|
final android.widget.ScrollView sv = new android.widget.ScrollView(this);
|
|
final TextView tv = new TextView(this);
|
|
tv.setTextSize(9);
|
|
tv.setTextColor(0xFFDDDDDD);
|
|
tv.setTypeface(android.graphics.Typeface.MONOSPACE);
|
|
int p = Ui.dp(this, 6);
|
|
tv.setPadding(p, p, p, p);
|
|
String log = com.minamp.net.DiagLog.dump();
|
|
tv.setText(log.length() > 0 ? log : "Nothing logged yet.");
|
|
sv.addView(tv);
|
|
AlertDialog.Builder b = new AlertDialog.Builder(this)
|
|
.setTitle("Connection log")
|
|
.setView(sv)
|
|
.setNegativeButton("Save to SD card", new DialogInterface.OnClickListener() {
|
|
public void onClick(DialogInterface d, int w) { saveDiagnostics(); }
|
|
})
|
|
.setPositiveButton("Close", null);
|
|
if (app.library != null) {
|
|
b.setNeutralButton("Test streaming", new DialogInterface.OnClickListener() {
|
|
public void onClick(DialogInterface d, int w) { testStreaming(); }
|
|
});
|
|
}
|
|
AlertDialog dlg = b.create();
|
|
dlg.show();
|
|
sv.post(new Runnable() { public void run() { sv.fullScroll(View.FOCUS_DOWN); } });
|
|
}
|
|
|
|
private void saveDiagnostics() {
|
|
try {
|
|
if (!android.os.Environment.MEDIA_MOUNTED.equals(android.os.Environment.getExternalStorageState())) {
|
|
toast("No SD card available");
|
|
return;
|
|
}
|
|
java.io.File f = new java.io.File(android.os.Environment.getExternalStorageDirectory(), "Minamp-log.txt");
|
|
java.io.FileOutputStream o = new java.io.FileOutputStream(f);
|
|
String head = "Minamp " + com.minamp.plex.Plex.version + " on " + android.os.Build.MODEL
|
|
+ ", Android " + android.os.Build.VERSION.RELEASE + "\n";
|
|
o.write((head + com.minamp.net.DiagLog.dump()).getBytes("UTF-8"));
|
|
o.close();
|
|
toast("Saved as Minamp-log.txt on the SD card");
|
|
} catch (Exception e) {
|
|
toast("Couldn't save: " + msg(e));
|
|
}
|
|
}
|
|
|
|
/** Asks the server about every transcode variant for one track, and logs exactly what it says. */
|
|
private void testStreaming() {
|
|
final Library lib = lib();
|
|
final Item current = PlayQueue.get().current();
|
|
toast("Testing\u2026 (about 30 s)");
|
|
run(new Task<Object>() {
|
|
public Object work() throws Exception {
|
|
com.minamp.net.DiagLog.log("=== streaming test ===");
|
|
Item t = current;
|
|
if (t == null) {
|
|
List<Item> r = lib.libraryRadio();
|
|
if (r.isEmpty()) throw new Exception("no tracks in the library");
|
|
t = r.get(0);
|
|
}
|
|
lib.ensureMedia(t);
|
|
com.minamp.net.DiagLog.log("track: " + t.title + " [" + t.codec + "/" + t.container + " " + t.bitrate + " kbps] via "
|
|
+ (lib.server.active() == null ? "?" : Server.kindLabel(lib.server.active().kind)));
|
|
int kbps = app.currentMaxKbps();
|
|
if (kbps <= 0) kbps = 320;
|
|
for (int v = 0; v < Library.VARIANTS; v++) {
|
|
com.minamp.net.DiagLog.log("variant " + v + " (" + Library.VARIANT_NAMES[v] + ") decision: " + lib.decision(t, kbps, v));
|
|
String session = "minamp-test-" + System.currentTimeMillis() + "-" + v;
|
|
String url = lib.transcodeUrl(t, kbps, 0, session, v);
|
|
com.minamp.net.Http.Response r = null;
|
|
try {
|
|
r = com.minamp.net.Http.get(url, null, 30000);
|
|
if (r.code < 400) {
|
|
byte[] buf = new byte[4096];
|
|
int n = r.body.read(buf);
|
|
boolean mp3 = n > 3 && ((buf[0] == 'I' && buf[1] == 'D' && buf[2] == '3') || ((buf[0] & 0xFF) == 0xFF && (buf[1] & 0xE0) == 0xE0));
|
|
com.minamp.net.DiagLog.log("variant " + v + " start: " + r.code + ", " + r.header("content-type")
|
|
+ ", first bytes " + (mp3 ? "look like MP3" : "are NOT MP3"));
|
|
} else {
|
|
com.minamp.net.DiagLog.log("variant " + v + " start: " + r.code + " " + com.minamp.plex.Plex.snippet(r.readString()));
|
|
}
|
|
} catch (Exception e) {
|
|
com.minamp.net.DiagLog.log("variant " + v + " start failed: " + com.minamp.net.Threads.describe(e));
|
|
} finally {
|
|
com.minamp.net.Http.abort(r);
|
|
lib.stopTranscode(session);
|
|
}
|
|
}
|
|
com.minamp.net.DiagLog.log("=== end of test ===");
|
|
return null;
|
|
}
|
|
public void done(Object o) { showDiagnostics(); }
|
|
public void fail(Exception e) {
|
|
com.minamp.net.DiagLog.log("streaming test failed: " + msg(e));
|
|
showDiagnostics();
|
|
}
|
|
});
|
|
}
|
|
|
|
private static String qualityLabel(int kbps) {
|
|
for (int i = 0; i < App.QUALITIES.length; i++) if (App.QUALITIES[i] == kbps) return App.QUALITY_LABELS[i];
|
|
return kbps + " kbps";
|
|
}
|
|
|
|
private void pickQuality(final String key, int current, String title) {
|
|
int sel = 0;
|
|
for (int i = 0; i < App.QUALITIES.length; i++) if (App.QUALITIES[i] == current) sel = i;
|
|
new AlertDialog.Builder(this)
|
|
.setTitle(title)
|
|
.setSingleChoiceItems(App.QUALITY_LABELS, sel, new DialogInterface.OnClickListener() {
|
|
public void onClick(DialogInterface d, int which) {
|
|
app.prefs.edit().putInt(key, App.QUALITIES[which]).commit();
|
|
d.dismiss();
|
|
toast("Applies from the next track");
|
|
}
|
|
}).show();
|
|
}
|
|
|
|
private void confirmSignOut() {
|
|
new AlertDialog.Builder(this)
|
|
.setMessage("Sign out and forget this server?")
|
|
.setPositiveButton("Sign out", new DialogInterface.OnClickListener() {
|
|
public void onClick(DialogInterface d, int w) {
|
|
PlayerService.send(BaseActivity.this, PlayerService.ACTION_STOP);
|
|
PlayQueue.get().clear();
|
|
app.signOut();
|
|
startActivity(new Intent(BaseActivity.this, SetupActivity.class).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
|
|
finish();
|
|
}
|
|
})
|
|
.setNegativeButton("Cancel", null)
|
|
.show();
|
|
}
|
|
}
|