Private
Public Access
Import minamp 1.0 source
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
0df8124629
194 files changed
+7319
No files matched your search
@@ -0,0 +1,448 @@
|
||||
package com.minamp;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
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 = Executors.newFixedThreadPool(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 OutOfMemoryError e) {
|
||||
app.images.clearMemory();
|
||||
app.handler.post(new Runnable() { public void run() { if (!destroyed) t.fail(new Exception("Out of memory")); } });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void toast(String s) { Toast.makeText(this, s, 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();
|
||||
}
|
||||
|
||||
@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,
|
||||
"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: confirmSignOut(); break;
|
||||
}
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
|
||||
protected void onConnectionChanged() {}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user