- Translate the interface to Galician (tools/translate_gl.py), including Galician month abbreviations for dates, which Android 2.3 lacks - Resize for the Galaxy Mini's 240x320 screen: larger text, rows, tabs and buttons; all sizes now live in res/values/dimens.xml - Redraw play/pause/refresh icons for every density; slightly brighter secondary text colours Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
521 lines
20 KiB
Java
521 lines
20 KiB
Java
package com.miniabs;
|
|
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
import org.json.JSONArray;
|
|
import org.json.JSONObject;
|
|
|
|
import android.app.AlertDialog;
|
|
import android.content.DialogInterface;
|
|
import android.content.Intent;
|
|
import android.os.Bundle;
|
|
import android.view.ContextMenu;
|
|
import android.view.Menu;
|
|
import android.view.MenuItem;
|
|
import android.view.View;
|
|
import android.widget.AdapterView;
|
|
import android.widget.ListView;
|
|
import android.widget.ProgressBar;
|
|
import android.widget.TextView;
|
|
|
|
public class HomeActivity extends BaseActivity {
|
|
static final int TAB_LATEST = 0, TAB_SHOWS = 1, TAB_SAVED = 2;
|
|
private static final int PAGE = 20;
|
|
|
|
// Kept across screen changes so going back is instant on a slow phone.
|
|
private static List<String[]> podcastLibs;
|
|
private static List<Rows.Row> latestCache, showsCache;
|
|
private static int latestPage;
|
|
private static boolean latestMore;
|
|
|
|
private ListView list;
|
|
private TextView empty, library;
|
|
private ProgressBar loading;
|
|
private Rows adapter;
|
|
private TextView[] tabs;
|
|
private int tab;
|
|
private boolean loadingLatest, loadingShows;
|
|
private String latestError, showsError;
|
|
private String savedSignature = "";
|
|
private int ticks;
|
|
|
|
static void clearCache() {
|
|
podcastLibs = null;
|
|
latestCache = null;
|
|
showsCache = null;
|
|
latestPage = 0;
|
|
latestMore = false;
|
|
}
|
|
|
|
@Override
|
|
protected void onCreate(Bundle b) {
|
|
super.onCreate(b);
|
|
if (!Prefs.get(this).loggedIn()) {
|
|
startActivity(new Intent(this, LoginActivity.class));
|
|
finish();
|
|
return;
|
|
}
|
|
setContentView(R.layout.activity_home);
|
|
list = (ListView) findViewById(R.id.list);
|
|
empty = (TextView) findViewById(R.id.empty);
|
|
loading = (ProgressBar) findViewById(R.id.loading);
|
|
library = (TextView) findViewById(R.id.library);
|
|
adapter = new Rows(this);
|
|
list.setAdapter(adapter);
|
|
registerForContextMenu(list);
|
|
setupMiniBar();
|
|
|
|
tabs = new TextView[]{(TextView) findViewById(R.id.tab_latest), (TextView) findViewById(R.id.tab_shows),
|
|
(TextView) findViewById(R.id.tab_saved)};
|
|
for (int i = 0; i < tabs.length; i++) {
|
|
final int t = i;
|
|
tabs[i].setOnClickListener(new View.OnClickListener() {
|
|
public void onClick(View v) {
|
|
selectTab(t);
|
|
}
|
|
});
|
|
}
|
|
library.setOnClickListener(new View.OnClickListener() {
|
|
public void onClick(View v) {
|
|
chooseLibrary();
|
|
}
|
|
});
|
|
findViewById(R.id.refresh).setOnClickListener(new View.OnClickListener() {
|
|
public void onClick(View v) {
|
|
refresh();
|
|
}
|
|
});
|
|
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
|
public void onItemClick(AdapterView<?> parent, View view, int pos, long id) {
|
|
onRowClick(adapter.getItem(pos));
|
|
}
|
|
});
|
|
|
|
updateLibraryLabel();
|
|
int start = getIntent().getIntExtra("tab", Prefs.get(this).sp.getInt("tab", TAB_LATEST));
|
|
selectTab(start);
|
|
}
|
|
|
|
@Override
|
|
protected void onNewIntent(Intent intent) {
|
|
super.onNewIntent(intent);
|
|
if (intent.hasExtra("tab")) selectTab(intent.getIntExtra("tab", TAB_LATEST));
|
|
}
|
|
|
|
@Override
|
|
protected void onResume() {
|
|
super.onResume();
|
|
if (adapter == null) return;
|
|
if (!Prefs.get(this).loggedIn()) { // logged out from Settings
|
|
startActivity(new Intent(this, LoginActivity.class));
|
|
finish();
|
|
return;
|
|
}
|
|
updateLibraryLabel();
|
|
if (tab == TAB_SAVED) showSaved();
|
|
else if ((tab == TAB_LATEST && latestCache == null && !loadingLatest)
|
|
|| (tab == TAB_SHOWS && showsCache == null && !loadingShows)) selectTab(tab); // signed in again
|
|
else adapter.notifyDataSetChanged();
|
|
}
|
|
|
|
@Override
|
|
protected void onTick() {
|
|
super.onTick();
|
|
if (adapter == null) return;
|
|
ticks++;
|
|
if (tab == TAB_SAVED) {
|
|
Episode cur = DownloadService.current;
|
|
String sig = (cur == null ? "" : cur.id) + "/" + DownloadService.queued().size()
|
|
+ "/" + DownloadService.lastError;
|
|
if (!sig.equals(savedSignature)) showSaved();
|
|
else if (cur != null) adapter.notifyDataSetChanged();
|
|
} else if (ticks % 3 == 0) {
|
|
adapter.notifyDataSetChanged();
|
|
}
|
|
}
|
|
|
|
private void updateLibraryLabel() {
|
|
String name = Prefs.get(this).libraryName();
|
|
library.setText(name.length() > 0 ? name : "");
|
|
}
|
|
|
|
// ------------------------------------------------------------------ tabs
|
|
|
|
private void selectTab(int t) {
|
|
if (t < 0 || t > 2) t = TAB_LATEST;
|
|
tab = t;
|
|
Prefs.get(this).sp.edit().putInt("tab", t).commit();
|
|
for (int i = 0; i < tabs.length; i++) {
|
|
tabs[i].setSelected(i == t);
|
|
tabs[i].setTextColor(getResources().getColor(i == t ? R.color.text : R.color.sub));
|
|
}
|
|
adapter.showPodcastName = t != TAB_SHOWS;
|
|
if (t == TAB_LATEST) {
|
|
if (latestCache != null) show(latestCache, null);
|
|
else if (!loadingLatest) loadLatest(false);
|
|
else show(null, null);
|
|
} else if (t == TAB_SHOWS) {
|
|
if (showsCache != null) show(showsCache, null);
|
|
else if (!loadingShows) loadShows();
|
|
else show(null, null);
|
|
} else {
|
|
showSaved();
|
|
}
|
|
list.setSelection(0);
|
|
}
|
|
|
|
/** rows == null means "still loading". */
|
|
private void show(List<Rows.Row> rows, String emptyText) {
|
|
boolean busy = rows == null && ((tab == TAB_LATEST && loadingLatest) || (tab == TAB_SHOWS && loadingShows));
|
|
String err = tab == TAB_LATEST ? latestError : tab == TAB_SHOWS ? showsError : null;
|
|
adapter.set(rows == null ? new ArrayList<Rows.Row>() : rows);
|
|
loading.setVisibility(busy ? View.VISIBLE : View.GONE);
|
|
String msg = null;
|
|
if (!busy && (rows == null || rows.isEmpty())) msg = err != null ? err : emptyText;
|
|
empty.setText(msg == null ? "" : msg);
|
|
empty.setVisibility(msg == null ? View.GONE : View.VISIBLE);
|
|
}
|
|
|
|
private final Runnable retryRefresh = new Runnable() {
|
|
public void run() {
|
|
refresh();
|
|
}
|
|
};
|
|
|
|
private void refresh() {
|
|
App.invalidate();
|
|
Covers.clearMemory();
|
|
latestCache = null;
|
|
showsCache = null;
|
|
podcastLibs = null;
|
|
latestError = null;
|
|
showsError = null;
|
|
if (tab == TAB_SHOWS) loadShows();
|
|
else if (tab == TAB_LATEST) loadLatest(false);
|
|
else {
|
|
showSaved();
|
|
new Job<Integer>() {
|
|
Integer work() {
|
|
return App.flushPending(HomeActivity.this);
|
|
}
|
|
|
|
void done(Integer n) {
|
|
if (n > 0) toast("Sincronizouse o progreso gardado sen conexión");
|
|
}
|
|
}.start();
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------ data
|
|
|
|
/** Picks a podcast library (the saved one if it still exists). Runs in the background. */
|
|
private String ensureLibrary() throws Exception {
|
|
Prefs p = Prefs.get(this);
|
|
if (podcastLibs == null) {
|
|
JSONArray libs = Api.libraries(this);
|
|
List<String[]> found = new ArrayList<String[]>();
|
|
for (int i = 0; i < libs.length(); i++) {
|
|
JSONObject l = libs.optJSONObject(i);
|
|
if (l != null && "podcast".equals(Util.str(l, "mediaType"))) {
|
|
found.add(new String[]{Util.str(l, "id"), Util.str(l, "name")});
|
|
}
|
|
}
|
|
if (found.isEmpty()) throw new IOException("Este servidor non ten bibliotecas de podcasts.");
|
|
podcastLibs = found;
|
|
}
|
|
String id = p.libraryId();
|
|
for (String[] l : podcastLibs) {
|
|
if (l[0].equals(id)) {
|
|
if (!l[1].equals(p.libraryName())) p.setLibrary(l[0], l[1]);
|
|
return id;
|
|
}
|
|
}
|
|
String[] first = podcastLibs.get(0);
|
|
p.setLibrary(first[0], first[1]);
|
|
return first[0];
|
|
}
|
|
|
|
private void loadLatest(final boolean more) {
|
|
loadingLatest = true;
|
|
latestError = null;
|
|
if (!more && tab == TAB_LATEST) show(null, null);
|
|
final int page = more ? latestPage + 1 : 0;
|
|
new Job<List<Rows.Row>>() {
|
|
boolean hasMore;
|
|
|
|
List<Rows.Row> work() throws Exception {
|
|
String lib = ensureLibrary();
|
|
List<Rows.Row> rows = new ArrayList<Rows.Row>();
|
|
if (!more) {
|
|
App.flushPending(HomeActivity.this);
|
|
App.loadProgress(HomeActivity.this);
|
|
List<Rows.Row> cont = new ArrayList<Rows.Row>();
|
|
try {
|
|
JSONArray items = Api.itemsInProgress(HomeActivity.this).optJSONArray("libraryItems");
|
|
for (int i = 0; items != null && i < items.length(); i++) {
|
|
JSONObject it = items.optJSONObject(i);
|
|
if (it == null || !lib.equals(Util.str(it, "libraryId"))) continue;
|
|
JSONObject media = it.optJSONObject("media");
|
|
JSONObject md = media == null ? null : media.optJSONObject("metadata");
|
|
Episode e = Episode.fromApi(it.optJSONObject("recentEpisode"), Util.str(it, "id"),
|
|
Util.str(md, "title"), Util.str(md, "author"));
|
|
if (e == null) continue;
|
|
double[] p = App.progress(e.id);
|
|
if (p != null && p[2] > 0) continue;
|
|
cont.add(Rows.Row.episode(e));
|
|
if (cont.size() >= 5) break;
|
|
}
|
|
} catch (Api.AuthException ae) {
|
|
throw ae;
|
|
} catch (Exception ignored) {
|
|
// older servers: just skip this section
|
|
}
|
|
if (!cont.isEmpty()) {
|
|
rows.add(Rows.Row.header("CONTINUAR ESCOITANDO"));
|
|
rows.addAll(cont);
|
|
}
|
|
rows.add(Rows.Row.header("ÚLTIMOS EPISODIOS"));
|
|
}
|
|
JSONArray eps = Api.recentEpisodes(HomeActivity.this, lib, page, PAGE).optJSONArray("episodes");
|
|
int n = eps == null ? 0 : eps.length();
|
|
for (int i = 0; i < n; i++) {
|
|
Episode e = Episode.fromApi(eps.optJSONObject(i), null, null, null);
|
|
if (e != null) rows.add(Rows.Row.episode(e));
|
|
}
|
|
hasMore = n >= PAGE;
|
|
return rows;
|
|
}
|
|
|
|
void done(List<Rows.Row> rows) {
|
|
loadingLatest = false;
|
|
if (isFinishing()) return;
|
|
updateLibraryLabel();
|
|
if (more && latestCache != null) {
|
|
int last = latestCache.size() - 1;
|
|
if (last >= 0 && latestCache.get(last).type == Rows.MORE) latestCache.remove(last);
|
|
latestCache.addAll(rows);
|
|
} else {
|
|
latestCache = rows;
|
|
}
|
|
latestPage = page;
|
|
latestMore = hasMore;
|
|
if (hasMore) latestCache.add(Rows.Row.more("Cargar máis"));
|
|
if (tab == TAB_LATEST) show(latestCache, "Aínda non hai episodios.");
|
|
}
|
|
|
|
void failed(Exception e) {
|
|
loadingLatest = false;
|
|
if (isFinishing()) return;
|
|
if (e instanceof Api.AuthException) {
|
|
showError(e);
|
|
return;
|
|
}
|
|
if (more) {
|
|
toast(Util.describe(e));
|
|
if (latestCache != null && !latestCache.isEmpty()) {
|
|
latestCache.get(latestCache.size() - 1).text = "Cargar máis";
|
|
adapter.notifyDataSetChanged();
|
|
}
|
|
return;
|
|
}
|
|
latestError = Util.describe(e) + "\n\nToca o botón de actualizar (arriba á dereita) para tentalo de novo.";
|
|
if (offerTrust(retryRefresh)) {
|
|
if (tab == TAB_LATEST) show(null, null);
|
|
return;
|
|
}
|
|
if (tab == TAB_LATEST) offlineFallback(e);
|
|
}
|
|
}.start();
|
|
}
|
|
|
|
private void loadShows() {
|
|
loadingShows = true;
|
|
showsError = null;
|
|
if (tab == TAB_SHOWS) show(null, null);
|
|
new Job<List<Rows.Row>>() {
|
|
List<Rows.Row> work() throws Exception {
|
|
String lib = ensureLibrary();
|
|
List<Rows.Row> rows = new ArrayList<Rows.Row>();
|
|
for (int page = 0; page < 20; page++) {
|
|
JSONObject r = Api.libraryItems(HomeActivity.this, lib, page, 100);
|
|
JSONArray res = r.optJSONArray("results");
|
|
int n = res == null ? 0 : res.length();
|
|
for (int i = 0; i < n; i++) {
|
|
JSONObject it = res.optJSONObject(i);
|
|
if (it == null) continue;
|
|
JSONObject media = it.optJSONObject("media");
|
|
JSONObject md = media == null ? null : media.optJSONObject("metadata");
|
|
Rows.Show s = new Rows.Show();
|
|
s.id = Util.str(it, "id");
|
|
s.title = Util.str(md, "title");
|
|
s.author = Util.str(md, "author");
|
|
s.episodes = media == null ? 0 : media.optInt("numEpisodes", 0);
|
|
if (s.episodes == 0 && media != null && media.optJSONArray("episodes") != null) {
|
|
s.episodes = media.optJSONArray("episodes").length();
|
|
}
|
|
rows.add(Rows.Row.show(s));
|
|
}
|
|
int total = r.optInt("total", 0);
|
|
if (n < 100 || rows.size() >= total) break;
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
void done(List<Rows.Row> rows) {
|
|
loadingShows = false;
|
|
if (isFinishing()) return;
|
|
updateLibraryLabel();
|
|
showsCache = rows;
|
|
if (tab == TAB_SHOWS) show(rows, "Non hai podcasts nesta biblioteca.");
|
|
}
|
|
|
|
void failed(Exception e) {
|
|
loadingShows = false;
|
|
if (isFinishing()) return;
|
|
if (e instanceof Api.AuthException) {
|
|
showError(e);
|
|
return;
|
|
}
|
|
showsError = Util.describe(e) + "\n\nToca o botón de actualizar (arriba á dereita) para tentalo de novo.";
|
|
if (offerTrust(retryRefresh)) {
|
|
if (tab == TAB_SHOWS) show(null, null);
|
|
return;
|
|
}
|
|
if (tab == TAB_SHOWS) offlineFallback(e);
|
|
}
|
|
}.start();
|
|
}
|
|
|
|
/** When the server can't be reached but episodes are saved, show those instead. */
|
|
private void offlineFallback(Exception e) {
|
|
if (!(e instanceof Api.ApiException) && !Downloads.list(this).isEmpty()) {
|
|
toast("Non se pode acceder ao servidor. Amósanse os episodios gardados.");
|
|
selectTab(TAB_SAVED);
|
|
} else {
|
|
show(null, null);
|
|
}
|
|
}
|
|
|
|
private void showSaved() {
|
|
List<Rows.Row> rows = new ArrayList<Rows.Row>();
|
|
Episode cur = DownloadService.current;
|
|
List<Episode> queued = DownloadService.queued();
|
|
savedSignature = (cur == null ? "" : cur.id) + "/" + queued.size() + "/" + DownloadService.lastError;
|
|
if (DownloadService.lastError != null) rows.add(Rows.Row.header(DownloadService.lastError));
|
|
if (cur != null || !queued.isEmpty()) {
|
|
rows.add(Rows.Row.header("DESCARGANDO"));
|
|
if (cur != null) rows.add(Rows.Row.episode(cur));
|
|
for (Episode q : queued) rows.add(Rows.Row.episode(q));
|
|
}
|
|
List<Episode> saved = Downloads.list(this);
|
|
if (!saved.isEmpty()) {
|
|
rows.add(Rows.Row.header("GARDADOS NA TARXETA SD (" + saved.size() + ")"));
|
|
for (Episode e : saved) rows.add(Rows.Row.episode(e));
|
|
}
|
|
if (rows.isEmpty()) {
|
|
show(rows, "Aínda non gardaches nada.\n\nMantén premido un episodio e escolle Descargar para escoitalo sen conexión.");
|
|
} else {
|
|
show(rows, null);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------ interaction
|
|
|
|
private void onRowClick(Rows.Row r) {
|
|
if (r == null) return;
|
|
switch (r.type) {
|
|
case Rows.EPISODE:
|
|
playEpisode(r.ep);
|
|
break;
|
|
case Rows.SHOW:
|
|
startActivity(new Intent(this, PodcastActivity.class)
|
|
.putExtra("itemId", r.show.id).putExtra("title", r.show.title)
|
|
.putExtra("author", r.show.author));
|
|
break;
|
|
case Rows.MORE:
|
|
if (!loadingLatest && latestMore) {
|
|
r.text = "Cargando…";
|
|
adapter.notifyDataSetChanged();
|
|
loadLatest(true);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
private Episode menuEpisode;
|
|
|
|
@Override
|
|
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo info) {
|
|
AdapterView.AdapterContextMenuInfo mi = (AdapterView.AdapterContextMenuInfo) info;
|
|
Rows.Row r = adapter.getItem(mi.position);
|
|
if (r.type != Rows.EPISODE) return;
|
|
menuEpisode = r.ep;
|
|
episodeMenu(menu, r.ep, true);
|
|
}
|
|
|
|
@Override
|
|
public boolean onContextItemSelected(MenuItem item) {
|
|
return onEpisodeContextItem(item, menuEpisode, new Runnable() {
|
|
public void run() {
|
|
if (tab == TAB_SAVED) showSaved();
|
|
else adapter.notifyDataSetChanged();
|
|
}
|
|
}) || super.onContextItemSelected(item);
|
|
}
|
|
|
|
private void chooseLibrary() {
|
|
if (podcastLibs == null || podcastLibs.size() < 2) {
|
|
toast(podcastLibs == null ? "Aínda se están cargando as bibliotecas…" : "Esta é a túa única biblioteca de podcasts.");
|
|
return;
|
|
}
|
|
String[] names = new String[podcastLibs.size()];
|
|
for (int i = 0; i < names.length; i++) names[i] = podcastLibs.get(i)[1];
|
|
new AlertDialog.Builder(this).setTitle("Biblioteca de podcasts")
|
|
.setItems(names, new DialogInterface.OnClickListener() {
|
|
public void onClick(DialogInterface d, int which) {
|
|
String[] l = podcastLibs.get(which);
|
|
Prefs.get(HomeActivity.this).setLibrary(l[0], l[1]);
|
|
updateLibraryLabel();
|
|
latestCache = null;
|
|
showsCache = null;
|
|
selectTab(tab == TAB_SAVED ? TAB_LATEST : tab);
|
|
}
|
|
}).show();
|
|
}
|
|
|
|
// ------------------------------------------------------------------ menu key
|
|
|
|
@Override
|
|
public boolean onCreateOptionsMenu(Menu menu) {
|
|
menu.add(0, 1, 0, "Actualizar").setIcon(android.R.drawable.ic_menu_rotate);
|
|
menu.add(0, 2, 0, "Biblioteca").setIcon(android.R.drawable.ic_menu_agenda);
|
|
menu.add(0, 3, 0, "En reprodución").setIcon(android.R.drawable.ic_media_play);
|
|
menu.add(0, 4, 0, "Axustes").setIcon(android.R.drawable.ic_menu_preferences);
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean onOptionsItemSelected(MenuItem item) {
|
|
switch (item.getItemId()) {
|
|
case 1:
|
|
refresh();
|
|
return true;
|
|
case 2:
|
|
chooseLibrary();
|
|
return true;
|
|
case 3:
|
|
startActivity(new Intent(this, PlayerActivity.class));
|
|
return true;
|
|
case 4:
|
|
startActivity(new Intent(this, SettingsActivity.class));
|
|
return true;
|
|
default:
|
|
return super.onOptionsItemSelected(item);
|
|
}
|
|
}
|
|
}
|