Private
Public Access
- Run network/TLS work on 256 KB-stack threads (net/Threads.java): Android 2.x's ~16 KB default stack overflowed during the bundled TLS handshake - Close pooled TLS connections off the caller thread - Try several Plex music-transcoder URL variants until the server accepts one, and remember the one that worked - Accept pasted server URLs with paths; default to 443 for https - Catch fatal errors and save a crash report (shown on next launch and written to Minamp-error.txt on the SD card) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
430 lines
19 KiB
Java
430 lines
19 KiB
Java
package com.minamp;
|
||
|
||
import java.util.ArrayList;
|
||
import java.util.List;
|
||
import java.util.Random;
|
||
|
||
import android.app.AlertDialog;
|
||
import android.content.DialogInterface;
|
||
import android.content.Intent;
|
||
import android.os.Bundle;
|
||
import android.view.LayoutInflater;
|
||
import android.view.Menu;
|
||
import android.view.MenuItem;
|
||
import android.view.View;
|
||
import android.view.ViewGroup;
|
||
import android.widget.AdapterView;
|
||
import android.widget.HorizontalScrollView;
|
||
import android.widget.ImageView;
|
||
import android.widget.LinearLayout;
|
||
import android.widget.ListView;
|
||
import android.widget.TextView;
|
||
|
||
import com.minamp.plex.Item;
|
||
import com.minamp.plex.Library;
|
||
import com.minamp.plex.Server;
|
||
import com.minamp.util.Json;
|
||
|
||
public class HomeActivity extends BaseActivity {
|
||
|
||
private static final int TAB_HOME = 0, TAB_ARTISTS = 1, TAB_ALBUMS = 2, TAB_PLAYLISTS = 3;
|
||
private static final String[] ALBUM_SORTS = {"titleSort", "artist.titleSort", "addedAt:desc", "year:desc", "viewCount:desc"};
|
||
private static final String[] ALBUM_SORT_LABELS = {"Title", "Artist", "Recently added", "Year (newest)", "Most played"};
|
||
|
||
private TextView[] tabs;
|
||
private View tabLine;
|
||
private ListView artistsList, albumsList, playlistsList;
|
||
private ItemAdapter artists, albums, playlists;
|
||
private LinearLayout homeContent;
|
||
private int tab = -1;
|
||
private boolean[] loaded = new boolean[4];
|
||
|
||
@Override
|
||
protected void onCreate(Bundle b) {
|
||
super.onCreate(b);
|
||
if (!app.isConfigured()) {
|
||
startActivity(new Intent(this, SetupActivity.class));
|
||
finish();
|
||
return;
|
||
}
|
||
setContentView(R.layout.activity_home);
|
||
showCrashReportIfAny();
|
||
setupTopBar(app.sectionTitle(), connectionLine(), false);
|
||
barAction(R.id.btn_action, Glyph.SEARCH, new View.OnClickListener() {
|
||
public void onClick(View v) { startActivity(new Intent(HomeActivity.this, SearchActivity.class)); }
|
||
});
|
||
barAction(R.id.btn_action2, Glyph.MORE, new View.OnClickListener() {
|
||
public void onClick(View v) { openOptionsMenu(); }
|
||
});
|
||
|
||
tabs = new TextView[] {
|
||
(TextView) findViewById(R.id.tab_home), (TextView) findViewById(R.id.tab_artists),
|
||
(TextView) findViewById(R.id.tab_albums), (TextView) findViewById(R.id.tab_playlists)
|
||
};
|
||
tabLine = findViewById(R.id.tab_line);
|
||
for (int i = 0; i < tabs.length; i++) {
|
||
final int t = i;
|
||
tabs[i].setOnClickListener(new View.OnClickListener() { public void onClick(View v) { showTab(t); } });
|
||
}
|
||
homeContent = (LinearLayout) findViewById(R.id.home_content);
|
||
artistsList = (ListView) findViewById(R.id.list_artists);
|
||
albumsList = (ListView) findViewById(R.id.list_albums);
|
||
playlistsList = (ListView) findViewById(R.id.list_playlists);
|
||
artists = new ItemAdapter(this, ItemAdapter.MODE_DEFAULT);
|
||
albums = new ItemAdapter(this, ItemAdapter.MODE_DEFAULT);
|
||
playlists = new ItemAdapter(this, ItemAdapter.MODE_DEFAULT);
|
||
wire(artistsList, artists);
|
||
wire(albumsList, albums);
|
||
wire(playlistsList, playlists);
|
||
artists.setPager(new ItemAdapter.Pager() { public void loadMore(int from) { loadArtists(from); } });
|
||
albums.setPager(new ItemAdapter.Pager() { public void loadMore(int from) { loadAlbums(from); } });
|
||
|
||
Server.listener = new Server.Listener() {
|
||
public void onConnectionChanged(Server s) {
|
||
app.handler.post(new Runnable() { public void run() { if (!destroyed) setBarSub(connectionLine()); } });
|
||
}
|
||
};
|
||
showTab(app.prefs.getInt("tab", TAB_HOME));
|
||
}
|
||
|
||
@Override
|
||
protected void onNewIntent(Intent intent) {
|
||
super.onNewIntent(intent);
|
||
// library or server may have changed in setup
|
||
if (!app.isConfigured()) { startActivity(new Intent(this, SetupActivity.class)); finish(); return; }
|
||
setupTopBar(app.sectionTitle(), connectionLine(), false);
|
||
loaded = new boolean[4];
|
||
int t = tab;
|
||
tab = -1;
|
||
showTab(t);
|
||
}
|
||
|
||
@Override
|
||
protected void onConnectionChanged() { setBarSub(connectionLine()); }
|
||
|
||
private String connectionLine() {
|
||
Server s = app.server;
|
||
if (s == null) return "";
|
||
Server.Conn c = s.active();
|
||
return s.name + (c != null ? " · " + Server.kindLabel(c.kind) : "");
|
||
}
|
||
|
||
private void wire(final ListView lv, final ItemAdapter ad) {
|
||
lv.setAdapter(ad);
|
||
lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||
public void onItemClick(AdapterView<?> p, View v, int pos, long id) { open(ad.getItem(pos), null, 0, null); }
|
||
});
|
||
lv.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
|
||
public boolean onItemLongClick(AdapterView<?> p, View v, int pos, long id) { itemMenu(ad.getItem(pos)); return true; }
|
||
});
|
||
}
|
||
|
||
private void showTab(int t) {
|
||
if (t == tab) return;
|
||
tab = t;
|
||
app.prefs.edit().putInt("tab", t).commit();
|
||
int accent = getResources().getColor(R.color.accent), dim = getResources().getColor(R.color.text2);
|
||
for (int i = 0; i < tabs.length; i++) tabs[i].setTextColor(i == t ? accent : dim);
|
||
int w = getResources().getDisplayMetrics().widthPixels / 4;
|
||
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) tabLine.getLayoutParams();
|
||
lp.width = w;
|
||
lp.leftMargin = w * t;
|
||
tabLine.setLayoutParams(lp);
|
||
findViewById(R.id.home_scroll).setVisibility(t == TAB_HOME ? View.VISIBLE : View.GONE);
|
||
artistsList.setVisibility(t == TAB_ARTISTS ? View.VISIBLE : View.GONE);
|
||
albumsList.setVisibility(t == TAB_ALBUMS ? View.VISIBLE : View.GONE);
|
||
playlistsList.setVisibility(t == TAB_PLAYLISTS ? View.VISIBLE : View.GONE);
|
||
hideStatus();
|
||
if (!loaded[t]) {
|
||
loaded[t] = true;
|
||
switch (t) {
|
||
case TAB_HOME: buildHome(); break;
|
||
case TAB_ARTISTS: loadArtists(0); break;
|
||
case TAB_ALBUMS: loadAlbums(0); break;
|
||
case TAB_PLAYLISTS: loadPlaylists(); break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ------------------------------------------------------------------ home tab
|
||
|
||
private void buildHome() {
|
||
homeContent.removeAllViews();
|
||
LayoutInflater inf = getLayoutInflater();
|
||
int pad = Ui.dp(this, 8);
|
||
|
||
LinearLayout row1 = tileRow();
|
||
row1.addView(tile(inf, Glyph.RADIO, "Library radio", "Shuffle everything", new View.OnClickListener() {
|
||
public void onClick(View v) {
|
||
final Library lib = lib();
|
||
runRadio("Library radio", new Task<List<Item>>() { public List<Item> work() throws Exception { return lib.libraryRadio(); } });
|
||
}
|
||
}));
|
||
row1.addView(spacer());
|
||
row1.addView(tile(inf, Glyph.HEART, "Loved radio", "Your top rated", new View.OnClickListener() {
|
||
public void onClick(View v) {
|
||
final Library lib = lib();
|
||
runRadio("Loved radio", new Task<List<Item>>() { public List<Item> work() throws Exception { return lib.topRatedRadio(); } });
|
||
}
|
||
}));
|
||
homeContent.addView(row1);
|
||
|
||
LinearLayout row2 = tileRow();
|
||
row2.addView(tile(inf, Glyph.CLOCK, "Time travel", "Pick a decade", new View.OnClickListener() {
|
||
public void onClick(View v) { timeTravel(); }
|
||
}));
|
||
row2.addView(spacer());
|
||
row2.addView(tile(inf, Glyph.SHUFFLE, "Random album", "Feeling lucky", new View.OnClickListener() {
|
||
public void onClick(View v) { randomAlbum(); }
|
||
}));
|
||
homeContent.addView(row2);
|
||
|
||
addShelf(inf, "Recently added", lib().recentlyAddedPath(20), ALBUM_SORTS[2]);
|
||
addShelf(inf, "Recently played", lib().recentlyPlayedPath(20), null);
|
||
homeContent.setPadding(0, pad / 2, 0, pad);
|
||
}
|
||
|
||
private LinearLayout tileRow() {
|
||
LinearLayout r = new LinearLayout(this);
|
||
r.setOrientation(LinearLayout.HORIZONTAL);
|
||
int p = Ui.dp(this, 8);
|
||
r.setPadding(p, p / 2, p, p / 2);
|
||
return r;
|
||
}
|
||
|
||
private View spacer() {
|
||
View v = new View(this);
|
||
v.setLayoutParams(new LinearLayout.LayoutParams(Ui.dp(this, 8), 1));
|
||
return v;
|
||
}
|
||
|
||
private View tile(LayoutInflater inf, int glyph, String title, String sub, View.OnClickListener l) {
|
||
View t = inf.inflate(R.layout.view_home_tile, null);
|
||
t.setLayoutParams(new LinearLayout.LayoutParams(0, Ui.dp(this, 56), 1f));
|
||
Ui.icon((ImageView) t.findViewById(R.id.tile_icon), glyph, getResources().getColor(R.color.accent), 22);
|
||
((TextView) t.findViewById(R.id.tile_title)).setText(title);
|
||
((TextView) t.findViewById(R.id.tile_sub)).setText(sub);
|
||
t.setOnClickListener(l);
|
||
return t;
|
||
}
|
||
|
||
private void addShelf(LayoutInflater inf, final String title, final String path, final String seeAllSort) {
|
||
LinearLayout head = new LinearLayout(this);
|
||
head.setOrientation(LinearLayout.HORIZONTAL);
|
||
TextView h = (TextView) inf.inflate(R.layout.row_header, null);
|
||
h.setText(title.toUpperCase());
|
||
head.addView(h, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
||
if (seeAllSort != null) {
|
||
TextView all = (TextView) inf.inflate(R.layout.row_header, null);
|
||
all.setText("SEE ALL ›");
|
||
all.setTextColor(getResources().getColor(R.color.text2));
|
||
all.setPadding(Ui.dp(this, 10), Ui.dp(this, 12), Ui.dp(this, 10), Ui.dp(this, 4));
|
||
all.setBackgroundResource(R.drawable.bg_pressable);
|
||
all.setOnClickListener(new View.OnClickListener() {
|
||
public void onClick(View v) {
|
||
app.prefs.edit().putInt("albumSort", 2).commit();
|
||
loaded[TAB_ALBUMS] = false;
|
||
showTab(TAB_ALBUMS);
|
||
}
|
||
});
|
||
head.addView(all, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||
}
|
||
homeContent.addView(head);
|
||
|
||
HorizontalScrollView hs = new HorizontalScrollView(this);
|
||
hs.setHorizontalScrollBarEnabled(false);
|
||
hs.setHorizontalFadingEdgeEnabled(false);
|
||
final LinearLayout strip = new LinearLayout(this);
|
||
strip.setOrientation(LinearLayout.HORIZONTAL);
|
||
strip.setPadding(Ui.dp(this, 6), 0, Ui.dp(this, 6), 0);
|
||
hs.addView(strip);
|
||
final TextView loading = new TextView(this);
|
||
loading.setText("Loading…");
|
||
loading.setTextColor(getResources().getColor(R.color.text2));
|
||
loading.setTextSize(12);
|
||
loading.setPadding(Ui.dp(this, 6), Ui.dp(this, 30), 0, Ui.dp(this, 30));
|
||
strip.addView(loading);
|
||
homeContent.addView(hs);
|
||
|
||
final Library lib = lib();
|
||
run(new Task<List<Item>>() {
|
||
public List<Item> work() throws Exception { return lib.server.items(path); }
|
||
public void done(List<Item> items) {
|
||
strip.removeAllViews();
|
||
if (items.isEmpty()) { loading.setText("Nothing here yet"); strip.addView(loading); return; }
|
||
LayoutInflater li = getLayoutInflater();
|
||
int art = Ui.dp(HomeActivity.this, 88);
|
||
for (int i = 0; i < items.size(); i++) {
|
||
final Item it = items.get(i);
|
||
View card = li.inflate(R.layout.view_album_card, strip, false);
|
||
((TextView) card.findViewById(R.id.card_title)).setText(it.title);
|
||
((TextView) card.findViewById(R.id.card_sub)).setText(it.artist);
|
||
app.images.load((ImageView) card.findViewById(R.id.card_art), it.thumb, art, R.drawable.art_placeholder);
|
||
card.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { open(it, null, 0, null); } });
|
||
card.setOnLongClickListener(new View.OnLongClickListener() { public boolean onLongClick(View v) { itemMenu(it); return true; } });
|
||
strip.addView(card);
|
||
}
|
||
}
|
||
public void fail(Exception e) {
|
||
loading.setText("Couldn't load: " + msg(e) + " (tap to retry)");
|
||
loading.setOnClickListener(new View.OnClickListener() {
|
||
public void onClick(View v) { loaded[TAB_HOME] = false; buildHome(); loaded[TAB_HOME] = true; }
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
private void timeTravel() {
|
||
final Library lib = lib();
|
||
toast("Finding decades…");
|
||
run(new Task<List<Integer>>() {
|
||
public List<Integer> work() throws Exception { return lib.decades(); }
|
||
public void done(final List<Integer> decades) {
|
||
if (decades.isEmpty()) { toast("No release years in this library"); return; }
|
||
String[] labels = new String[decades.size() + 1];
|
||
labels[0] = "Surprise me";
|
||
for (int i = 0; i < decades.size(); i++) labels[i + 1] = decades.get(i) + "s";
|
||
new AlertDialog.Builder(HomeActivity.this).setTitle("Time travel radio")
|
||
.setItems(labels, new DialogInterface.OnClickListener() {
|
||
public void onClick(DialogInterface d, int which) {
|
||
final int dec = which == 0 ? decades.get(new Random().nextInt(decades.size())).intValue()
|
||
: decades.get(which - 1).intValue();
|
||
runRadio(dec + "s radio", new Task<List<Item>>() {
|
||
public List<Item> work() throws Exception { return lib.timeTravelRadio(dec); }
|
||
});
|
||
}
|
||
}).show();
|
||
}
|
||
public void fail(Exception e) { toast(msg(e)); }
|
||
});
|
||
}
|
||
|
||
private void randomAlbum() {
|
||
final Library lib = lib();
|
||
run(new Task<List<Item>>() {
|
||
public List<Item> work() throws Exception { return lib.server.items(lib.albumsPath("random", 0, 1)); }
|
||
public void done(List<Item> l) {
|
||
if (l.isEmpty()) { toast("No albums"); return; }
|
||
Item a = l.get(0);
|
||
playContainer(a, false, 0);
|
||
open(a, null, 0, null);
|
||
}
|
||
public void fail(Exception e) { toast(msg(e)); }
|
||
});
|
||
}
|
||
|
||
// ------------------------------------------------------------------ list tabs
|
||
|
||
private int totalOf(Json.Obj mc, int fallback) {
|
||
if (mc == null) return fallback;
|
||
return mc.optInt("totalSize", fallback);
|
||
}
|
||
|
||
private void loadArtists(final int from) {
|
||
final Library lib = lib();
|
||
if (from == 0) showStatus(true, "Loading artists…", null);
|
||
run(new Task<Object[]>() {
|
||
public Object[] work() throws Exception {
|
||
Json.Obj mc = lib.server.get(lib.artistsPath(from, 100));
|
||
return new Object[] { Item.listFrom(mc), Integer.valueOf(totalOf(mc, -1)) };
|
||
}
|
||
@SuppressWarnings("unchecked")
|
||
public void done(Object[] r) {
|
||
if (tab == TAB_ARTISTS) hideStatus();
|
||
List<Item> l = (List<Item>) r[0];
|
||
int total = ((Integer) r[1]).intValue();
|
||
if (from == 0) artists.set(l, total); else artists.append(l, total);
|
||
if (from == 0 && l.isEmpty() && tab == TAB_ARTISTS) showStatus(false, "No artists in this library", null);
|
||
}
|
||
public void fail(Exception e) {
|
||
artists.loadFailed();
|
||
if (from == 0 && tab == TAB_ARTISTS) showStatus(false, msg(e), new Runnable() { public void run() { loadArtists(0); } });
|
||
else toast(msg(e));
|
||
}
|
||
});
|
||
}
|
||
|
||
private void loadAlbums(final int from) {
|
||
final Library lib = lib();
|
||
final String sort = ALBUM_SORTS[Math.min(app.prefs.getInt("albumSort", 0), ALBUM_SORTS.length - 1)];
|
||
if (from == 0) showStatus(true, "Loading albums…", null);
|
||
run(new Task<Object[]>() {
|
||
public Object[] work() throws Exception {
|
||
Json.Obj mc = lib.server.get(lib.albumsPath(sort, from, 100));
|
||
return new Object[] { Item.listFrom(mc), Integer.valueOf(totalOf(mc, -1)) };
|
||
}
|
||
@SuppressWarnings("unchecked")
|
||
public void done(Object[] r) {
|
||
if (tab == TAB_ALBUMS) hideStatus();
|
||
List<Item> l = (List<Item>) r[0];
|
||
int total = ((Integer) r[1]).intValue();
|
||
if (from == 0) { albums.set(l, total); albumsList.setSelection(0); } else albums.append(l, total);
|
||
if (from == 0 && l.isEmpty() && tab == TAB_ALBUMS) showStatus(false, "No albums in this library", null);
|
||
}
|
||
public void fail(Exception e) {
|
||
albums.loadFailed();
|
||
if (from == 0 && tab == TAB_ALBUMS) showStatus(false, msg(e), new Runnable() { public void run() { loadAlbums(0); } });
|
||
else toast(msg(e));
|
||
}
|
||
});
|
||
}
|
||
|
||
private void loadPlaylists() {
|
||
final Library lib = lib();
|
||
showStatus(true, "Loading playlists…", null);
|
||
run(new Task<List<Item>>() {
|
||
public List<Item> work() throws Exception { return lib.server.items(lib.playlistsPath()); }
|
||
public void done(List<Item> l) {
|
||
if (tab == TAB_PLAYLISTS) hideStatus();
|
||
List<Item> audio = new ArrayList<Item>();
|
||
for (int i = 0; i < l.size(); i++) if (l.get(i).type == Item.PLAYLIST) audio.add(l.get(i));
|
||
playlists.set(audio, -1);
|
||
if (audio.isEmpty() && tab == TAB_PLAYLISTS) showStatus(false, "No music playlists yet", null);
|
||
}
|
||
public void fail(Exception e) {
|
||
if (tab == TAB_PLAYLISTS) showStatus(false, msg(e), new Runnable() { public void run() { loadPlaylists(); } });
|
||
}
|
||
});
|
||
}
|
||
|
||
// ------------------------------------------------------------------ menu
|
||
|
||
@Override
|
||
public boolean onCreateOptionsMenu(Menu menu) {
|
||
super.onCreateOptionsMenu(menu);
|
||
menu.add(0, 10, 0, "Sort albums");
|
||
menu.add(0, 11, 0, "Refresh");
|
||
return true;
|
||
}
|
||
|
||
@Override
|
||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||
MenuItem s = menu.findItem(10);
|
||
if (s != null) s.setVisible(tab == TAB_ALBUMS);
|
||
return super.onPrepareOptionsMenu(menu);
|
||
}
|
||
|
||
@Override
|
||
public boolean onOptionsItemSelected(MenuItem item) {
|
||
if (item.getItemId() == 10) {
|
||
new AlertDialog.Builder(this).setTitle("Sort albums by")
|
||
.setSingleChoiceItems(ALBUM_SORT_LABELS, app.prefs.getInt("albumSort", 0), new DialogInterface.OnClickListener() {
|
||
public void onClick(DialogInterface d, int which) {
|
||
d.dismiss();
|
||
app.prefs.edit().putInt("albumSort", which).commit();
|
||
loadAlbums(0);
|
||
}
|
||
}).show();
|
||
return true;
|
||
}
|
||
if (item.getItemId() == 11) {
|
||
loaded = new boolean[4];
|
||
int t = tab;
|
||
tab = -1;
|
||
showTab(t);
|
||
return true;
|
||
}
|
||
return super.onOptionsItemSelected(item);
|
||
}
|
||
}
|