Minamp 1.5: Galician interface

- Interface in Galician by default regardless of the phone's language, with
  English selectable under Axustes -> Idioma; switching rebuilds the screens
- All text in tools/strings.py (key -> English, Galician), generating
  res/values/strings.xml (gl) and res/values-en/strings.xml; layouts now use
  string resources
- util/Tr.java: translations for the network and Plex layers, which can't
  read Android resources (falls back to English in desktop tests)
- net/RefusedException: distinguish "server answered but refused" from
  connection failures
- The connection log stays in English for troubleshooting

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xockoandClaude Opus 5.5 committed 2026-10-15 00:11:41 +02:00
1 parent 6fd17bc9c5
commit fa67e95fde
29 files changed
+1029 -206

No files matched your search

+39
View File
@@ -49,6 +49,13 @@ public class App extends Application {
handler = new Handler();
prefs = getSharedPreferences("minamp", MODE_PRIVATE);
installCrashLogger();
applyLanguage(this);
com.minamp.util.Tr.source = new com.minamp.util.Tr.Source() {
public String get(String key) {
int id = getResources().getIdentifier("tr_" + key, "string", getPackageName());
return id == 0 ? null : getString(id);
}
};
// Android 2.2's HttpURLConnection pool is buggy; we use our own client, but be safe.
System.setProperty("http.keepAlive", "false");
@@ -91,6 +98,38 @@ public class App extends Application {
}, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
}
// ------------------------------------------------------------------ language
public static final String[] LANGUAGES = {"gl", "en"};
public static final String[] LANGUAGE_NAMES = {"Galego", "English"};
/** Galician unless the user picked English, whatever language the phone itself uses. */
public String language() { return prefs.getString("lang", "gl"); }
public static String languageName(String code) {
for (int i = 0; i < LANGUAGES.length; i++) if (LANGUAGES[i].equals(code)) return LANGUAGE_NAMES[i];
return code;
}
/** Forces the app's language on a context's resources (call before inflating any layout). */
public static void applyLanguage(android.content.Context c) {
App app = instance;
if (app == null) return;
java.util.Locale loc = new java.util.Locale(app.language());
java.util.Locale.setDefault(loc);
android.content.res.Resources r = c.getResources();
android.content.res.Configuration cfg = r.getConfiguration();
if (loc.equals(cfg.locale)) return;
cfg.locale = loc;
r.updateConfiguration(cfg, r.getDisplayMetrics());
}
@Override
public void onConfigurationChanged(android.content.res.Configuration newConfig) {
super.onConfigurationChanged(newConfig);
applyLanguage(this); // the system resets the locale on config changes
}
// ------------------------------------------------------------------ crash reports
private static final String CRASH_FILE = "crash.txt";
+93 -61
View File
@@ -64,6 +64,8 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
});
}
protected String str(int id, Object... args) { return args.length == 0 ? getString(id) : getString(id, args); }
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); }
@@ -72,8 +74,10 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
@Override
protected void onCreate(android.os.Bundle b) {
super.onCreate(b);
app = App.get();
App.applyLanguage(this);
App.applyLanguage(getApplicationContext());
super.onCreate(b);
}
/** If the last run hit an error, show it so it can be reported (also saved as Minamp-error.txt on the SD card). */
@@ -89,9 +93,9 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
tv.setPadding(p, p, p, p);
sv.addView(tv);
new AlertDialog.Builder(this)
.setTitle("Minamp hit an error last time")
.setTitle(R.string.error_last_time)
.setView(sv)
.setPositiveButton("OK", null)
.setPositiveButton(R.string.ok, null)
.show();
}
@@ -254,7 +258,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
}
protected void playList(List<Item> tracks, int start, boolean shuffle, String title) {
if (tracks.isEmpty()) { toast("Nothing to play"); return; }
if (tracks.isEmpty()) { toast(str(R.string.nothing_to_play)); return; }
PlayQueue.get().set(tracks, start, shuffle, title);
PlayerService.playCurrent(this, 0);
}
@@ -263,14 +267,14 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
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 + "…");
toast(how == 0 ? str(shuffle ? R.string.shuffling_x : R.string.playing_x, it.title) : str(R.string.adding_x, 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; }
if (tracks.isEmpty()) { toast(str(R.string.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"); }
if (how == 1) { PlayQueue.get().playNext(tracks); toast(str(R.string.playing_next)); }
else if (how == 2) { PlayQueue.get().append(tracks); toast(str(R.string.added_to_queue)); }
else { playList(tracks, shuffle ? -1 : 0, shuffle, it.title); return; }
if (wasEmpty) PlayerService.playCurrent(BaseActivity.this, 0);
}
@@ -279,11 +283,11 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
}
protected void runRadio(final String title, final Task<List<Item>> fetch) {
toast("Starting " + title + "…");
toast(str(R.string.starting_x, 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; }
if (tracks.isEmpty()) { toast(str(R.string.nothing_found_for, title)); return; }
playList(tracks, 0, false, title);
}
public void fail(Exception e) { toast(msg(e)); }
@@ -295,17 +299,17 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
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);
labels.add(str(R.string.play_next)); actions.add(1);
labels.add(str(R.string.add_to_queue)); actions.add(2);
if (it.albumKey.length() > 0) { labels.add(str(R.string.go_to_album)); actions.add(5); }
if (it.artistKey.length() > 0) { labels.add(str(R.string.go_to_artist)); actions.add(6); }
labels.add(str(it.loved() ? R.string.unlove : R.string.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); }
labels.add(str(R.string.play)); actions.add(0);
labels.add(str(it.type == Item.ARTIST ? R.string.artist_radio_shuffle : R.string.shuffle)); actions.add(3);
labels.add(str(R.string.play_next)); actions.add(1);
labels.add(str(R.string.add_to_queue)); actions.add(2);
if (it.type == Item.ALBUM && it.artistKey.length() > 0) { labels.add(str(R.string.go_to_artist)); actions.add(6); }
} else return;
new AlertDialog.Builder(this)
.setTitle(it.title)
@@ -327,7 +331,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
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");
toast(str(action == 1 ? R.string.playing_next : R.string.added_to_queue));
} else {
playContainer(it, false, action);
}
@@ -357,11 +361,11 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
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 done(Object o) { toast(str(love ? R.string.loved : R.string.unloved)); }
public void fail(Exception e) {
it.userRating = old;
if (after != null) after.run();
toast("Couldn't save rating: " + msg(e));
toast(str(R.string.cant_save_rating, msg(e)));
}
});
}
@@ -370,9 +374,9 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
@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");
menu.add(0, 1, 0, R.string.now_playing);
menu.add(0, 2, 0, R.string.search);
menu.add(0, 3, 0, R.string.settings);
return true;
}
@@ -388,53 +392,54 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
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)");
String conn = s == null || s.active() == null ? str(R.string.not_connected_yet) : Server.kindLabel(s.active().kind)
+ str(s.active().uri.startsWith("https") ? R.string.secure : R.string.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"),
"Encrypt at home too (slower): " + (Server.preferSecureAtHome ? "On" : "Off"),
"Library: " + app.sectionTitle(),
"Server: " + (s == null ? "-" : s.name),
"Connection: " + conn,
"Connection log & streaming test",
"Sign out",
str(R.string.set_quality_home, qualityLabel(app.qualityHome())),
str(R.string.set_quality_away, qualityLabel(app.qualityAway())),
str(R.string.set_download_first, str(app.downloadFirst() ? R.string.on : R.string.off)),
str(R.string.set_secure_home, str(Server.preferSecureAtHome ? R.string.on : R.string.off)),
str(R.string.set_library, app.sectionTitle()),
str(R.string.set_server, s == null ? "-" : s.name),
str(R.string.set_connection, conn),
str(R.string.set_log),
str(R.string.set_language, App.languageName(app.language())),
str(R.string.sign_out),
};
new AlertDialog.Builder(this)
.setTitle("Settings")
.setTitle(R.string.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 0: pickQuality("qHome", app.qualityHome(), str(R.string.quality_home_title)); break;
case 1: pickQuality("qAway", app.qualityAway(), str(R.string.quality_away_title)); break;
case 2:
app.prefs.edit().putBoolean("downloadFirst", !app.downloadFirst()).commit();
toast("Download before playing: " + (app.downloadFirst() ? "on" : "off"));
toast(str(R.string.download_first_toast, str(app.downloadFirst() ? R.string.on : R.string.off)));
break;
case 3:
Server.preferSecureAtHome = !Server.preferSecureAtHome;
app.prefs.edit().putBoolean("secureHome", Server.preferSecureAtHome).commit();
if (app.server != null) app.server.invalidate();
toast(Server.preferSecureAtHome ? "Using HTTPS at home (each new connection is slower on this phone)"
: "Using plain HTTP at home (faster)");
toast(str(Server.preferSecureAtHome ? R.string.https_home_on : R.string.https_home_off));
break;
case 4: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_LIBRARY)); break;
case 5: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_SERVER)); break;
case 6:
if (app.server != null) {
app.server.invalidate();
toast("Reconnecting…");
toast(str(R.string.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 done(Server.Conn c) { toast(str(R.string.connected_x, Server.kindLabel(c.kind))); onConnectionChanged(); }
public void fail(Exception e) { toast(msg(e)); }
});
}
break;
case 7: showDiagnostics(); break;
case 8: confirmSignOut(); break;
case 8: pickLanguage(); break;
case 9: confirmSignOut(); break;
}
}
}).show();
@@ -454,17 +459,17 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
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.");
tv.setText(log.length() > 0 ? log : str(R.string.log_empty));
sv.addView(tv);
AlertDialog.Builder b = new AlertDialog.Builder(this)
.setTitle("Connection log")
.setTitle(R.string.connection_log)
.setView(sv)
.setNegativeButton("Save to SD card", new DialogInterface.OnClickListener() {
.setNegativeButton(R.string.save_to_sd, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) { saveDiagnostics(); }
})
.setPositiveButton("Close", null);
.setPositiveButton(R.string.close, null);
if (app.library != null) {
b.setNeutralButton("Test streaming", new DialogInterface.OnClickListener() {
b.setNeutralButton(R.string.test_streaming, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) { testStreaming(); }
});
}
@@ -476,7 +481,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
private void saveDiagnostics() {
try {
if (!android.os.Environment.MEDIA_MOUNTED.equals(android.os.Environment.getExternalStorageState())) {
toast("No SD card available");
toast(str(R.string.no_sd));
return;
}
java.io.File f = new java.io.File(android.os.Environment.getExternalStorageDirectory(), "Minamp-log.txt");
@@ -485,9 +490,9 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
+ ", 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");
toast(str(R.string.saved_log));
} catch (Exception e) {
toast("Couldn't save: " + msg(e));
toast(str(R.string.cant_save, msg(e)));
}
}
@@ -495,7 +500,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
private void testStreaming() {
final Library lib = lib();
final Item current = PlayQueue.get().current();
toast("Testing\u2026 (about 30 s)");
toast(str(R.string.testing));
run(new Task<Object>() {
public Object work() throws Exception {
com.minamp.net.DiagLog.log("=== streaming test ===");
@@ -559,29 +564,56 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
});
}
private static String qualityLabel(int kbps) {
private String qualityLabel(int kbps) {
if (kbps <= 0) return str(R.string.quality_original);
for (int i = 0; i < App.QUALITIES.length; i++) if (App.QUALITIES[i] == kbps) return App.QUALITY_LABELS[i];
return kbps + " kbps";
}
private String[] qualityLabels() {
String[] l = App.QUALITY_LABELS.clone();
l[0] = str(R.string.quality_original);
return l;
}
private void pickLanguage() {
int sel = 0;
for (int i = 0; i < App.LANGUAGES.length; i++) if (App.LANGUAGES[i].equals(app.language())) sel = i;
new AlertDialog.Builder(this)
.setTitle(R.string.language_title)
.setSingleChoiceItems(App.LANGUAGE_NAMES, sel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int which) {
d.dismiss();
if (App.LANGUAGES[which].equals(app.language())) return;
app.prefs.edit().putString("lang", App.LANGUAGES[which]).commit();
App.applyLanguage(getApplicationContext());
App.applyLanguage(BaseActivity.this);
// rebuild the screens in the new language
startActivity(new Intent(BaseActivity.this, HomeActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP).putExtra("relaunch", true));
finish();
}
}).show();
}
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() {
.setSingleChoiceItems(qualityLabels(), 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");
toast(str(R.string.applies_next_track));
}
}).show();
}
private void confirmSignOut() {
new AlertDialog.Builder(this)
.setMessage("Sign out and forget this server?")
.setPositiveButton("Sign out", new DialogInterface.OnClickListener() {
.setMessage(R.string.sign_out_confirm)
.setPositiveButton(R.string.sign_out, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
PlayerService.send(BaseActivity.this, PlayerService.ACTION_STOP);
PlayQueue.get().clear();
@@ -590,7 +622,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
finish();
}
})
.setNegativeButton("Cancel", null)
.setNegativeButton(R.string.cancel, null)
.show();
}
}
+8 -8
View File
@@ -63,7 +63,7 @@ public class BrowseActivity extends BaseActivity {
Button play = (Button) header.findViewById(R.id.detail_play);
Button shuffle = (Button) header.findViewById(R.id.detail_shuffle);
if (item.type == Item.ARTIST) { play.setText("Play all"); shuffle.setText("Artist radio"); }
if (item.type == Item.ARTIST) { play.setText(R.string.play_all); shuffle.setText(R.string.artist_radio); }
play.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { playAll(false); } });
shuffle.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { playAll(true); } });
@@ -73,9 +73,9 @@ public class BrowseActivity extends BaseActivity {
private String kindLabel() {
switch (item.type) {
case Item.ARTIST: return "Artist";
case Item.ALBUM: return "Album";
case Item.PLAYLIST: return "Playlist";
case Item.ARTIST: return str(R.string.artist);
case Item.ALBUM: return str(R.string.album);
case Item.PLAYLIST: return str(R.string.playlist);
default: return "";
}
}
@@ -95,13 +95,13 @@ public class BrowseActivity extends BaseActivity {
private void setMeta(List<Item> children) {
StringBuilder sb = new StringBuilder();
if (item.type == Item.ARTIST) {
sb.append(children.size()).append(children.size() == 1 ? " album" : " albums");
sb.append(str(children.size() == 1 ? R.string.n_albums_one : R.string.n_albums, children.size()));
} else {
long total = 0;
int n = 0;
for (int i = 0; i < children.size(); i++) if (children.get(i).type == Item.TRACK) { n++; total += children.get(i).duration; }
if (item.year > 0) sb.append(item.year).append(" · ");
sb.append(n).append(n == 1 ? " track" : " tracks");
sb.append(str(n == 1 ? R.string.n_tracks_one : R.string.n_tracks, n));
if (total > 0) sb.append(" · ").append(Ui.longDuration(total));
}
((TextView) header.findViewById(R.id.detail_meta)).setText(sb.toString());
@@ -110,7 +110,7 @@ public class BrowseActivity extends BaseActivity {
private void load() {
final Library lib = lib();
final Item target = item;
showStatus(true, "Loading…", null);
showStatus(true, str(R.string.loading), null);
run(new Task<Object[]>() {
public Object[] work() throws Exception {
Item full = null;
@@ -137,7 +137,7 @@ public class BrowseActivity extends BaseActivity {
adapter.setAlbumArtist(item.artist);
adapter.set(children, -1);
setMeta(children);
if (children.isEmpty()) showStatus(false, "Nothing here", null);
if (children.isEmpty()) showStatus(false, str(R.string.nothing_here), null);
}
public void fail(Exception e) {
showStatus(false, msg(e), new Runnable() { public void run() { load(); } });
+40 -30
View File
@@ -29,7 +29,7 @@ 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 static final int[] ALBUM_SORT_LABELS = {R.string.sort_title, R.string.sort_artist, R.string.sort_added, R.string.sort_year, R.string.sort_played};
private TextView[] tabs;
private View tabLine;
@@ -90,6 +90,14 @@ public class HomeActivity extends BaseActivity {
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (intent.getBooleanExtra("relaunch", false)) {
// language changed: rebuild from scratch so every inflated text is re-read
finish();
overridePendingTransition(0, 0);
startActivity(new Intent(this, HomeActivity.class));
overridePendingTransition(0, 0);
return;
}
// library or server may have changed in setup
if (!app.isConfigured()) { startActivity(new Intent(this, SetupActivity.class)); finish(); return; }
setupTopBar(app.sectionTitle(), connectionLine(), false);
@@ -154,33 +162,33 @@ public class HomeActivity extends BaseActivity {
int pad = Ui.dp(this, 8);
LinearLayout row1 = tileRow();
row1.addView(tile(inf, Glyph.RADIO, "Library radio", "Shuffle everything", new View.OnClickListener() {
row1.addView(tile(inf, Glyph.RADIO, str(R.string.library_radio), str(R.string.library_radio_sub), 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(); } });
runRadio(str(R.string.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() {
row1.addView(tile(inf, Glyph.HEART, str(R.string.loved_radio), str(R.string.loved_radio_sub), 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(); } });
runRadio(str(R.string.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() {
row2.addView(tile(inf, Glyph.CLOCK, str(R.string.time_travel), str(R.string.time_travel_sub), 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() {
row2.addView(tile(inf, Glyph.SHUFFLE, str(R.string.random_album), str(R.string.random_album_sub), 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);
addShelf(inf, str(R.string.recently_added), lib().recentlyAddedPath(20), ALBUM_SORTS[2]);
addShelf(inf, str(R.string.recently_played), lib().recentlyPlayedPath(20), null);
homeContent.setPadding(0, pad / 2, 0, pad);
}
@@ -216,7 +224,7 @@ public class HomeActivity extends BaseActivity {
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.setText(R.string.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);
@@ -239,7 +247,7 @@ public class HomeActivity extends BaseActivity {
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.setText(R.string.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));
@@ -251,7 +259,7 @@ public class HomeActivity extends BaseActivity {
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; }
if (items.isEmpty()) { loading.setText(R.string.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++) {
@@ -266,7 +274,7 @@ public class HomeActivity extends BaseActivity {
}
}
public void fail(Exception e) {
loading.setText("Couldn't load: " + msg(e) + " (tap to retry)");
loading.setText(str(R.string.cant_load_tap, msg(e)));
loading.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) { loaded[TAB_HOME] = false; buildHome(); loaded[TAB_HOME] = true; }
});
@@ -276,20 +284,20 @@ public class HomeActivity extends BaseActivity {
private void timeTravel() {
final Library lib = lib();
toast("Finding decades…");
toast(str(R.string.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; }
if (decades.isEmpty()) { toast(str(R.string.no_years)); 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")
labels[0] = str(R.string.surprise_me);
for (int i = 0; i < decades.size(); i++) labels[i + 1] = str(R.string.decade_x, decades.get(i));
new AlertDialog.Builder(HomeActivity.this).setTitle(R.string.time_travel_title)
.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>>() {
runRadio(str(R.string.decade_radio_x, dec), new Task<List<Item>>() {
public List<Item> work() throws Exception { return lib.timeTravelRadio(dec); }
});
}
@@ -304,7 +312,7 @@ public class HomeActivity extends BaseActivity {
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; }
if (l.isEmpty()) { toast(str(R.string.no_albums)); return; }
Item a = l.get(0);
playContainer(a, false, 0);
open(a, null, 0, null);
@@ -322,7 +330,7 @@ public class HomeActivity extends BaseActivity {
private void loadArtists(final int from) {
final Library lib = lib();
if (from == 0) showStatus(true, "Loading artists…", null);
if (from == 0) showStatus(true, str(R.string.loading_artists), null);
run(new Task<Object[]>() {
public Object[] work() throws Exception {
Json.Obj mc = lib.server.get(lib.artistsPath(from, 100));
@@ -334,7 +342,7 @@ public class HomeActivity extends BaseActivity {
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);
if (from == 0 && l.isEmpty() && tab == TAB_ARTISTS) showStatus(false, str(R.string.no_artists_library), null);
}
public void fail(Exception e) {
artists.loadFailed();
@@ -347,7 +355,7 @@ public class HomeActivity extends BaseActivity {
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);
if (from == 0) showStatus(true, str(R.string.loading_albums), null);
run(new Task<Object[]>() {
public Object[] work() throws Exception {
Json.Obj mc = lib.server.get(lib.albumsPath(sort, from, 100));
@@ -359,7 +367,7 @@ public class HomeActivity extends BaseActivity {
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);
if (from == 0 && l.isEmpty() && tab == TAB_ALBUMS) showStatus(false, str(R.string.no_albums_library), null);
}
public void fail(Exception e) {
albums.loadFailed();
@@ -371,7 +379,7 @@ public class HomeActivity extends BaseActivity {
private void loadPlaylists() {
final Library lib = lib();
showStatus(true, "Loading playlists…", null);
showStatus(true, str(R.string.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) {
@@ -379,7 +387,7 @@ public class HomeActivity extends BaseActivity {
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);
if (audio.isEmpty() && tab == TAB_PLAYLISTS) showStatus(false, str(R.string.no_playlists), null);
}
public void fail(Exception e) {
if (tab == TAB_PLAYLISTS) showStatus(false, msg(e), new Runnable() { public void run() { loadPlaylists(); } });
@@ -392,8 +400,8 @@ public class HomeActivity extends BaseActivity {
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
menu.add(0, 10, 0, "Sort albums");
menu.add(0, 11, 0, "Refresh");
menu.add(0, 10, 0, R.string.sort_albums);
menu.add(0, 11, 0, R.string.refresh);
return true;
}
@@ -407,8 +415,10 @@ public class HomeActivity extends BaseActivity {
@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() {
String[] sortLabels = new String[ALBUM_SORT_LABELS.length];
for (int i = 0; i < sortLabels.length; i++) sortLabels[i] = str(ALBUM_SORT_LABELS[i]);
new AlertDialog.Builder(this).setTitle(R.string.sort_albums_by)
.setSingleChoiceItems(sortLabels, app.prefs.getInt("albumSort", 0), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int which) {
d.dismiss();
app.prefs.edit().putInt("albumSort", which).commit();
+9 -8
View File
@@ -87,7 +87,7 @@ public class NowPlayingActivity extends BaseActivity {
public void onClick(View v) {
PlayQueue.get().cycleRepeat();
int r = PlayQueue.get().repeat();
toast(r == PlayQueue.REPEAT_ALL ? "Repeat queue" : r == PlayQueue.REPEAT_ONE ? "Repeat track" : "Repeat off");
toast(str(r == PlayQueue.REPEAT_ALL ? R.string.repeat_queue : r == PlayQueue.REPEAT_ONE ? R.string.repeat_track : R.string.repeat_off));
}
});
loveGlyph = Ui.glyph(this, Glyph.HEART_OUTLINE, white, 20); loveGlyph.scale(1f);
@@ -153,8 +153,8 @@ public class NowPlayingActivity extends BaseActivity {
Item it = PlayQueue.get().current();
PlayQueue q = PlayQueue.get();
if (it == null) {
title.setText("Nothing playing");
sub.setText("Pick something from your library");
title.setText(R.string.nothing_playing);
sub.setText(R.string.pick_something);
from.setText("");
art.setImageResource(R.drawable.art_placeholder);
shownThumb = null;
@@ -170,7 +170,7 @@ public class NowPlayingActivity extends BaseActivity {
loveGlyph.setType(it.loved() ? Glyph.HEART : Glyph.HEART_OUTLINE);
loveGlyph.setColor(it.loved() ? accent : white);
albumLink.setVisibility(it.albumKey.length() > 0 ? View.VISIBLE : View.INVISIBLE);
albumLink.setText("Album ›");
albumLink.setText(R.string.album_link);
if (!it.thumb.equals(shownThumb)) {
shownThumb = it.thumb;
@@ -203,9 +203,9 @@ public class NowPlayingActivity extends BaseActivity {
private void updateSleep() {
long at = PlayerService.sleepAt();
if (PlayerService.sleepEndOfTrack()) sleepText.setText("End of track");
if (PlayerService.sleepEndOfTrack()) sleepText.setText(R.string.end_of_track);
else if (at > 0) sleepText.setText(Ui.time(at - System.currentTimeMillis()));
else sleepText.setText("Sleep");
else sleepText.setText(R.string.sleep);
}
private void setBackdrop(int color) {
@@ -216,8 +216,9 @@ public class NowPlayingActivity extends BaseActivity {
private void sleepDialog() {
final int[] mins = {0, 15, 30, 45, 60, 90, -1};
String[] labels = {"Off", "15 minutes", "30 minutes", "45 minutes", "1 hour", "1½ hours", "End of this track"};
new AlertDialog.Builder(this).setTitle("Sleep timer")
String[] labels = {str(R.string.sleep_off), str(R.string.sleep_15), str(R.string.sleep_30), str(R.string.sleep_45),
str(R.string.sleep_60), str(R.string.sleep_90), str(R.string.sleep_eot)};
new AlertDialog.Builder(this).setTitle(R.string.sleep_timer)
.setItems(labels, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int which) {
startService(new Intent(NowPlayingActivity.this, PlayerService.class)
+20 -13
View File
@@ -146,6 +146,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
@Override
public void onCreate() {
super.onCreate();
App.applyLanguage(this);
instance = this;
audio = (AudioManager) getSystemService(AUDIO_SERVICE);
audio.registerMediaButtonEventReceiver(new ComponentName(this, MediaButtonReceiver.class));
@@ -261,7 +262,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
offsetMs = 0;
lastPos = posMs;
playWhenReady = play;
setState(BUFFERING, "Connecting…");
setState(BUFFERING, getString(R.string.connecting));
goForeground();
if (play) requestFocus();
final boolean download = forceDownload || app.downloadFirst();
@@ -297,8 +298,13 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
} catch (final Throwable e) {
if (!(e instanceof Exception)) App.logError("player", e);
final String m = com.minamp.net.Threads.describe(e);
final int code = e instanceof com.minamp.net.RefusedException ? ((com.minamp.net.RefusedException) e).code : 0;
post(new Runnable() {
public void run() { if (g == gen) trackFailed(m); }
public void run() {
if (g != gen) return;
if (code > 0) { refusal = m; refusalCode = code; }
trackFailed(m);
}
});
}
}
@@ -319,6 +325,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
File f = new File(base, "play-" + (g % 2) + ".tmp");
Http.Response r = null;
String why = null;
int lastCode = 0;
for (int i = 0; i < urls.size(); i++) {
r = Http.get(urls.get(i), App.get().server != null ? App.get().server.mediaHeaders() : Plex.headers(null), 30000);
if (r.code == 200) {
@@ -328,12 +335,13 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
}
break;
}
why = "Server refused the stream (" + r.code + ")";
why = com.minamp.util.Tr.t("refused", "Server refused the stream (%1$d)", r.code);
String body = new String(r.readAll(), "UTF-8");
if (body.trim().length() > 0) why += ": " + Plex.snippet(body);
lastCode = r.code;
r = null;
}
if (r == null) throw new IOException(why);
if (r == null) throw new com.minamp.net.RefusedException(lastCode, why);
InputStream in = r.body;
FileOutputStream out = new FileOutputStream(f);
try {
@@ -349,7 +357,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
lastUi = got;
final int pct = (int) Math.min(99, got * 100 / total);
final int fg = g;
post(new Runnable() { public void run() { if (fg == gen) setState(BUFFERING, "Downloading " + pct + "%"); } });
post(new Runnable() { public void run() { if (fg == gen) setState(BUFFERING, getString(R.string.downloading_pct, pct)); } });
}
}
} finally {
@@ -400,7 +408,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
mp.setDataSource(proxy.register(routeId, urls, ext, transcoding,
App.get().server != null ? App.get().server.mediaHeaders() : null));
}
setState(BUFFERING, "Buffering…");
setState(BUFFERING, getString(R.string.buffering));
mp.prepareAsync();
} catch (Exception e) {
trackFailed(message(e));
@@ -433,7 +441,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
Item it = track;
// A stream that ends well before the track does means the network dropped.
if (it != null && it.duration > 0 && pos > 0 && pos < it.duration - 15000 && !fromFile) {
trackFailed("Stream interrupted");
trackFailed(getString(R.string.stream_interrupted));
return;
}
if (it != null) {
@@ -443,7 +451,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
if (sleepEndOfTrack) {
sleepEndOfTrack = false;
PlayQueue.get().advance(false);
stopAll("Sleep timer");
stopAll(getString(R.string.sleep_timer));
return;
}
if (PlayQueue.get().advance(false)) {
@@ -457,15 +465,14 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
public boolean onError(MediaPlayer p, int what, int extra) {
if (p != mp) return true;
trackFailed("Playback error (" + what + "/" + extra + ")");
trackFailed(getString(R.string.playback_error, what, extra));
return true;
}
private void trackFailed(String msg) {
long pos = Math.max(currentPosition(), lastPos);
if (refusal != null) msg = refusal; // the server's own reason beats "Playback error (1/-1004)"
boolean permanent = refusalCode >= 400 && refusalCode < 500 && refusalCode != 408 && refusalCode != 429
|| (msg != null && msg.startsWith("Server refused the stream (4"));
boolean permanent = refusalCode >= 400 && refusalCode < 500 && refusalCode != 408 && refusalCode != 429;
refusal = null;
refusalCode = 0;
retries = permanent ? 99 : retries + 1;
@@ -593,7 +600,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
if (sleepAt > 0 && now >= sleepAt) {
sleepAt = 0;
pause();
toast("Sleep timer: paused");
toast(getString(R.string.sleep_paused));
}
if (state == PLAYING && track != null) {
long pos = currentPosition();
@@ -700,7 +707,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
n.flags |= Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR;
Intent open = new Intent(this, NowPlayingActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pi = PendingIntent.getActivity(this, 0, open, PendingIntent.FLAG_UPDATE_CURRENT);
String line = (state == PAUSED ? "Paused · " : "") + it.artist;
String line = (state == PAUSED ? getString(R.string.paused_dot) : "") + it.artist;
n.setLatestEventInfo(this, it.title, line, pi);
if (state == PAUSED || state == IDLE) {
// let Android reclaim us while paused, but keep the notification as a shortcut back
+7 -7
View File
@@ -18,11 +18,11 @@ public class QueueActivity extends BaseActivity {
protected void onCreate(Bundle b) {
super.onCreate(b);
setContentView(R.layout.activity_queue);
setupTopBar("Up next", "", true);
setupTopBar(str(R.string.up_next), "", true);
barAction(R.id.btn_action, Glyph.SHUFFLE, new View.OnClickListener() {
public void onClick(View v) {
PlayQueue.get().setShuffle(!PlayQueue.get().shuffle());
toast(PlayQueue.get().shuffle() ? "Shuffle on" : "Shuffle off");
toast(str(PlayQueue.get().shuffle() ? R.string.shuffle_on : R.string.shuffle_off));
}
});
barAction(R.id.btn_action2, Glyph.MORE, new View.OnClickListener() {
@@ -41,8 +41,8 @@ public class QueueActivity extends BaseActivity {
public boolean onItemLongClick(AdapterView<?> p, View v, final int pos, long id) {
final Item it = PlayQueue.get().at(pos);
final boolean isCurrent = pos == PlayQueue.get().index();
String[] opts = isCurrent ? new String[] {"Go to album", "Go to artist"}
: new String[] {"Play next", "Remove from queue", "Go to album", "Go to artist"};
String[] opts = isCurrent ? new String[] {str(R.string.go_to_album), str(R.string.go_to_artist)}
: new String[] {str(R.string.play_next), str(R.string.remove_from_queue), str(R.string.go_to_album), str(R.string.go_to_artist)};
new AlertDialog.Builder(QueueActivity.this).setTitle(it.title)
.setItems(opts, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
@@ -73,14 +73,14 @@ public class QueueActivity extends BaseActivity {
PlayQueue q = PlayQueue.get();
adapter.set(q.items(), -1);
adapter.setHighlight(q.index());
setBarSub(q.size() + (q.size() == 1 ? " track" : " tracks") + (q.title().length() > 0 ? " · " + q.title() : ""));
if (q.size() == 0) showStatus(false, "The queue is empty", null); else hideStatus();
setBarSub(str(q.size() == 1 ? R.string.n_tracks_one : R.string.n_tracks, q.size()) + (q.title().length() > 0 ? " \u00b7 " + q.title() : ""));
if (q.size() == 0) showStatus(false, str(R.string.queue_empty), null); else hideStatus();
if (scroll && q.index() > 0) list.setSelection(Math.max(0, q.index() - 1));
}
private void clearDialog() {
new AlertDialog.Builder(this)
.setItems(new String[] {"Clear upcoming tracks", "Clear everything"}, new DialogInterface.OnClickListener() {
.setItems(new String[] {str(R.string.clear_upcoming), str(R.string.clear_everything)}, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
PlayQueue q = PlayQueue.get();
if (w == 0) {
+5 -5
View File
@@ -43,7 +43,7 @@ public class SearchActivity extends BaseActivity {
public void onItemClick(AdapterView<?> p, View v, int pos, long id) {
Item it = adapter.getItem(pos);
hideKeyboard();
open(it, null, 0, "Search");
open(it, null, 0, str(R.string.search));
}
});
list.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@@ -75,7 +75,7 @@ public class SearchActivity extends BaseActivity {
return false;
}
});
showStatus(false, "Search " + app.sectionTitle(), null);
showStatus(false, str(R.string.search_in, app.sectionTitle()), null);
}
@Override
@@ -93,19 +93,19 @@ public class SearchActivity extends BaseActivity {
private void search(boolean force) {
final String q = box.getText().toString().trim();
if (q.length() < 2) { adapter.set(new java.util.ArrayList<Item>(), -1); showStatus(false, "Search " + app.sectionTitle(), null); return; }
if (q.length() < 2) { adapter.set(new java.util.ArrayList<Item>(), -1); showStatus(false, str(R.string.search_in, app.sectionTitle()), null); return; }
if (!force && q.equals(lastQuery)) return;
lastQuery = q;
final int my = ++seq;
final Library lib = lib();
showStatus(true, "Searching…", null);
showStatus(true, str(R.string.searching), null);
run(new Task<List<Item>>() {
public List<Item> work() throws Exception { return lib.search(q); }
public void done(List<Item> r) {
if (my != seq) return;
adapter.set(r, -1);
list.setSelection(0);
if (r.isEmpty()) showStatus(false, "Nothing found for “" + q + "”", null); else hideStatus();
if (r.isEmpty()) showStatus(false, str(R.string.nothing_found_q, q), null); else hideStatus();
}
public void fail(Exception e) {
if (my != seq) return;
+23 -23
View File
@@ -103,7 +103,7 @@ public class SetupActivity extends BaseActivity {
private void startPin() {
clearPendingPin();
showBusy("Contacting plex.tv\u2026\n(the first secure connection takes a few seconds on this phone)");
showBusy(str(R.string.contacting_plextv));
final int my = ++pinSeq;
run(new Task<Plex.Pin>() {
public Plex.Pin work() throws Exception { return Plex.createPin(); }
@@ -117,7 +117,7 @@ public class SetupActivity extends BaseActivity {
public void fail(Exception e) {
if (my != pinSeq) return;
show(welcome);
toast("Couldn't reach plex.tv: " + msg(e));
toast(str(R.string.cant_reach_plextv, msg(e)));
}
});
}
@@ -147,7 +147,7 @@ public class SetupActivity extends BaseActivity {
StringBuilder spaced = new StringBuilder();
for (int i = 0; i < code.length(); i++) { if (i > 0) spaced.append(' '); spaced.append(code.charAt(i)); }
pinCode.setText(spaced.toString());
pinStatus.setText("Waiting for you to link\u2026");
pinStatus.setText(R.string.pin_waiting);
show(pinPage);
int my = ++pinSeq;
checkPinNow(my);
@@ -170,7 +170,7 @@ public class SetupActivity extends BaseActivity {
if (my != pinSeq || destroyed || pinCheckRunning) return;
long age = System.currentTimeMillis() - pinStarted;
if (age > 14 * 60 * 1000) {
pinStatus.setText("Code expired \u2014 press Cancel and sign in again");
pinStatus.setText(R.string.pin_expired);
clearPendingPin();
return;
}
@@ -183,11 +183,11 @@ public class SetupActivity extends BaseActivity {
pinChecks++;
if (token == null) {
long left = (14 * 60 * 1000 - (System.currentTimeMillis() - pinStarted)) / 60000;
pinStatus.setText("Not linked yet \u00b7 checked " + pinChecks + "\u00d7 \u00b7 " + left + " min left");
pinStatus.setText(str(R.string.pin_not_yet, pinChecks, (int) left));
schedulePinCheck(my);
return;
}
pinStatus.setText("Linked!");
pinStatus.setText(R.string.pin_linked);
pinSeq++;
clearPendingPin();
signedIn(token);
@@ -196,14 +196,14 @@ public class SetupActivity extends BaseActivity {
pinCheckRunning = false;
if (my != pinSeq) return;
pinChecks++;
pinStatus.setText("Check " + pinChecks + " failed: " + msg(e) + " \u2014 retrying");
pinStatus.setText(str(R.string.pin_check_failed, pinChecks, msg(e)));
schedulePinCheck(my);
}
});
}
private void signedIn(final String account) {
showBusy("Signed in. Finding your servers…");
showBusy(str(R.string.signed_in_finding));
app.prefs.edit().putString("account", account).commit();
run(new Task<String>() {
public String work() throws Exception { return Plex.username(account); }
@@ -213,18 +213,18 @@ public class SetupActivity extends BaseActivity {
}
private void loadServers(final String account) {
showBusy("Finding your servers…");
showBusy(str(R.string.finding_servers));
run(new Task<List<Server>>() {
public List<Server> work() throws Exception { return Plex.servers(account); }
public void done(final List<Server> servers) {
if (servers.isEmpty()) {
show(welcome);
toast("No Plex Media Servers on this account");
toast(str(R.string.no_servers));
return;
}
if (servers.size() == 1) { pickServer(servers.get(0), null); return; }
chooseTitle.setText("Choose a server");
chooseSub.setText("Signed in as " + app.prefs.getString("username", "you"));
chooseTitle.setText(R.string.choose_server);
chooseSub.setText(str(R.string.signed_in_as, app.prefs.getString("username", str(R.string.you))));
chooseList.removeAllViews();
for (int i = 0; i < servers.size(); i++) {
final Server s = servers.get(i);
@@ -233,7 +233,7 @@ public class SetupActivity extends BaseActivity {
int kind = s.connections.get(k).kind;
if (kind == Server.LOCAL || kind == Server.LOCAL_HTTP) local++; else remote++;
}
String sub = (s.owned ? "Yours" : "Shared with you") + " · " + (local > 0 ? "home + " : "") + (remote > 0 ? "remote" : "local only");
String sub = str(s.owned ? R.string.server_yours : R.string.server_shared) + " \u00b7 " + (local > 0 ? str(R.string.server_home_plus) : "") + str(remote > 0 ? R.string.server_remote : R.string.server_local_only);
addChoice(s.name, sub, new View.OnClickListener() {
public void onClick(View v) { pickServer(s, new Runnable() { public void run() { done(servers); } }); }
});
@@ -243,7 +243,7 @@ public class SetupActivity extends BaseActivity {
}
public void fail(Exception e) {
show(welcome);
toast("Couldn't list servers: " + msg(e));
toast(str(R.string.cant_list_servers, msg(e)));
}
});
}
@@ -257,7 +257,7 @@ public class SetupActivity extends BaseActivity {
}
private void pickServer(final Server s, final Runnable back) {
showBusy("Connecting to " + s.name + "…\nTrying home network, then remote, then relay.");
showBusy(str(R.string.connecting_to, s.name));
run(new Task<Server.Conn>() {
public Server.Conn work() throws Exception { return s.pick(); }
public void done(Server.Conn c) {
@@ -275,12 +275,12 @@ public class SetupActivity extends BaseActivity {
private void connectManual() {
String url = ((EditText) findViewById(R.id.edit_url)).getText().toString().trim();
String token = ((EditText) findViewById(R.id.edit_token)).getText().toString().trim();
if (url.length() == 0) { toast("Enter your server's address"); return; }
if (url.length() == 0) { toast(str(R.string.enter_address)); return; }
final Server s = new Server();
s.name = "Plex server";
s.name = str(R.string.plex_server);
s.token = token;
s.add(Server.normalizeManual(url), Server.MANUAL);
showBusy("Connecting…");
showBusy(str(R.string.connecting));
run(new Task<String>() {
public String work() throws Exception {
s.pick();
@@ -294,7 +294,7 @@ public class SetupActivity extends BaseActivity {
public void fail(Exception e) {
show(manual);
boolean auth = e instanceof Plex.PlexException && ((Plex.PlexException) e).code == 401;
toast(auth ? "The server wants a token (or sign in with Plex instead)" : msg(e));
toast(auth ? str(R.string.server_wants_token) : msg(e));
}
});
}
@@ -302,22 +302,22 @@ public class SetupActivity extends BaseActivity {
// ------------------------------------------------------------------ libraries
private void loadSections(final Server s, final Runnable back) {
showBusy("Loading libraries…");
showBusy(str(R.string.loading_libraries));
run(new Task<List<Server.Section>>() {
public List<Server.Section> work() throws Exception { return s.musicSections(); }
public void done(List<Server.Section> secs) {
if (secs.isEmpty()) {
if (back != null) back.run(); else show(welcome);
toast(s.name + " has no music libraries");
toast(str(R.string.no_music_libraries, s.name));
return;
}
if (secs.size() == 1) { finishWith(s, secs.get(0)); return; }
chooseTitle.setText("Choose a music library");
chooseTitle.setText(R.string.choose_library);
chooseSub.setText(s.name);
chooseList.removeAllViews();
for (int i = 0; i < secs.size(); i++) {
final Server.Section sec = secs.get(i);
addChoice(sec.title, "Music", new View.OnClickListener() {
addChoice(sec.title, str(R.string.music), new View.OnClickListener() {
public void onClick(View v) { finishWith(s, sec); }
});
}
+3 -2
View File
@@ -43,8 +43,9 @@ public final class Ui {
public static String longDuration(long ms) {
long min = (ms + 30000) / 60000;
if (min < 60) return min + " min";
return (min / 60) + " h " + (min % 60) + " min";
App app = App.get();
if (min < 60) return app.getString(R.string.min, (int) min);
return app.getString(R.string.h_min, (int) (min / 60), (int) (min % 60));
}
/** Average colour of a bitmap, nudged towards something vivid but dark enough for white text. */
+5 -4
View File
@@ -7,6 +7,7 @@ import java.util.Date;
import java.util.List;
import java.util.Locale;
import com.minamp.util.Tr;
import com.minamp.bc.asn1.ASN1Encoding;
import com.minamp.bc.asn1.ASN1ObjectIdentifier;
import com.minamp.bc.asn1.ASN1OctetString;
@@ -69,7 +70,7 @@ public final class CertVerifier {
synchronized (verifiedChains) { known = verifiedChains.contains(chainKey); }
if (known) {
Certificate leaf = Certificate.getInstance(chainDer.get(0));
if (!hostMatches(leaf, host)) throw new IOException("Certificate does not match " + host);
if (!hostMatches(leaf, host)) throw new IOException(Tr.t("cert_mismatch", "Certificate does not match %1$s", host));
if (new Date().getTime() > 1704067200000L) checkDates(leaf, new Date());
return;
}
@@ -83,7 +84,7 @@ public final class CertVerifier {
Certificate leaf = chain.get(0);
if (!hostMatches(leaf, host)) {
throw new IOException("Certificate does not match " + host);
throw new IOException(Tr.t("cert_mismatch", "Certificate does not match %1$s", host));
}
Date now = new Date();
@@ -115,7 +116,7 @@ public final class CertVerifier {
if (!isCa(cand)) continue;
if (signatureValid(current, cand)) { next = cand; break; }
}
if (next == null) throw new IOException("Untrusted certificate chain for " + host);
if (next == null) throw new IOException(Tr.t("cert_untrusted", "Untrusted certificate chain for %1$s", host));
current = next;
}
throw new IOException("Certificate chain too long");
@@ -125,7 +126,7 @@ public final class CertVerifier {
Date from = c.getStartDate().getDate();
Date to = c.getEndDate().getDate();
if (now.before(from) || now.after(to)) {
throw new IOException("Certificate expired or not yet valid (check the phone's date)");
throw new IOException(Tr.t("cert_dates", "Certificate expired or not yet valid (check the phone's date)"));
}
}
+13
View File
@@ -0,0 +1,13 @@
package com.minamp.net;
import java.io.IOException;
/** The server answered, but refused the request (HTTP status in {@link #code}). */
public class RefusedException extends IOException {
public final int code;
public RefusedException(int code, String message) {
super(message);
this.code = code;
}
}
+3 -6
View File
@@ -197,10 +197,6 @@ public final class StreamProxy implements Runnable {
out.flush();
}
private static final class RefusedException extends IOException {
final int code;
RefusedException(int code, String msg) { super(msg); this.code = code; }
}
/** Tries the route's URLs in order; returns the accepted response or throws with the server's words. */
private Http.Response openUpstream(Route route, String method, String range, int first, int last) throws IOException {
@@ -223,9 +219,10 @@ public final class StreamProxy implements Runnable {
try { body = "HEAD".equals(method) ? "" : new String(up.readAll(), "UTF-8"); } catch (IOException ignored) {}
up.close();
code = up.code;
refusal = "Server refused the stream (" + up.code + ")" + (body.trim().length() > 0 ? ": " + com.minamp.plex.Plex.snippet(body) : "");
refusal = com.minamp.util.Tr.t("refused", "Server refused the stream (%1$d)", up.code)
+ (body.trim().length() > 0 ? ": " + com.minamp.plex.Plex.snippet(body) : "");
if (up.code == 503 && route.urls.get(i).indexOf("/library/parts/") >= 0) {
refusal = "Plex couldn't open this file (503). Is the drive or folder with your music available to the server?";
refusal = com.minamp.util.Tr.t("file_503", "Plex couldn't open this file (503). Is the drive or folder with your music available to the server?");
}
DiagLog.log("stream variant " + i + " refused: " + refusal);
}
+3 -2
View File
@@ -117,8 +117,9 @@ public final class Item {
switch (type) {
case TRACK: return artist + (album.length() > 0 ? " · " + album : "");
case ALBUM: return artist + (year > 0 ? " · " + year : "");
case PLAYLIST: return leafCount + (leafCount == 1 ? " track" : " tracks");
case ARTIST: return "Artist";
case PLAYLIST: return leafCount == 1 ? com.minamp.util.Tr.t("n_tracks_one", "%1$d track", leafCount)
: com.minamp.util.Tr.t("n_tracks", "%1$d tracks", leafCount);
case ARTIST: return com.minamp.util.Tr.t("artist", "Artist");
default: return "";
}
}
+1 -1
View File
@@ -147,7 +147,7 @@ public final class Library {
t.bitrate = full.bitrate; t.partSize = full.partSize;
if (t.duration == 0) t.duration = full.duration;
}
if (t.partKey.length() == 0) throw new IOException("Track has no playable media");
if (t.partKey.length() == 0) throw new IOException(com.minamp.util.Tr.t("no_media", "Track has no playable media"));
}
public static void shuffle(List<Item> l) {
+1 -1
View File
@@ -49,7 +49,7 @@ public final class Plex {
static Json.Obj fetchJson(String method, String url, String token, int timeout) throws IOException {
Http.Response r = Http.request(method, url, headers(token), "POST".equals(method) ? new byte[0] : null, timeout);
String body = r.readString();
if (r.code == 401) throw new PlexException(401, "Not authorized (401) on " + describePath(url));
if (r.code == 401) throw new PlexException(401, com.minamp.util.Tr.t("not_authorized", "Not authorized (401) on %1$s", describePath(url)));
if (r.code < 200 || r.code >= 300) {
String snippet = snippet(body);
com.minamp.net.DiagLog.log(" server said: " + snippet);
+7 -5
View File
@@ -7,6 +7,7 @@ import java.util.Comparator;
import java.util.List;
import com.minamp.net.Http;
import com.minamp.util.Tr;
import com.minamp.util.Json;
/**
@@ -104,10 +105,10 @@ public final class Server {
public static String kindLabel(int kind) {
switch (kind) {
case LOCAL: case LOCAL_HTTP: return "Home network";
case MANUAL: return "Direct";
case REMOTE: case REMOTE_HTTP: return "Remote";
case RELAY: return "Plex Relay";
case LOCAL: case LOCAL_HTTP: return Tr.t("kind_home", "Home network");
case MANUAL: return Tr.t("kind_direct", "Direct");
case REMOTE: case REMOTE_HTTP: return Tr.t("kind_remote", "Remote");
case RELAY: return Tr.t("kind_relay", "Plex Relay");
default: return "";
}
}
@@ -240,7 +241,8 @@ public final class Server {
if (err == null) return choose(c);
if (firstError == null) firstError = kindLabel(c.kind) + ": " + err;
}
throw new IOException("Can't reach " + (name.length() > 0 ? name : "server") + (firstError != null ? "\n" + firstError : ""));
throw new IOException(Tr.t("cant_reach", "Can't reach %1$s", name.length() > 0 ? name : Tr.t("server", "server"))
+ (firstError != null ? "\n" + firstError : ""));
}
// ------------------------------------------------------------------ requests
+25
View File
@@ -0,0 +1,25 @@
package com.minamp.util;
import java.util.Locale;
/**
* Translations for the pure-Java layers (network, Plex), which can't see Android resources.
* The app installs a {@link Source} that reads string resources named "tr_" + key; without one
* (desktop tests) the English text passed in is used.
*/
public final class Tr {
public interface Source { String get(String key); }
public static volatile Source source;
private Tr() {}
public static String t(String key, String english, Object... args) {
String f = null;
Source s = source;
if (s != null) {
try { f = s.get(key); } catch (Exception ignored) {}
}
if (f == null) f = english;
return args.length == 0 ? f : String.format(Locale.US, f, args);
}
}