Files
plexamp-lite/src/com/minamp/HomeActivity.java
T
xockoandClaude Opus 5.5 0a2ac901bd Minamp 1.1
- 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>
2026-10-14 19:59:58 +02:00

430 lines
19 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}