Files
plexamp-lite/src/com/minamp/App.java
T
xockoandClaude Opus 5.5 6fd17bc9c5 Minamp 1.4: faster connecting
- Remember the connection that worked and try it alone first next start
- Prefer plain HTTP on the home network (each TLS handshake costs seconds on a
  600 MHz phone); new setting "Encrypt at home too" to keep HTTPS
- Try secure addresses one at a time instead of in parallel, so they don't
  all time out together
- TLS 1.2 with session resumption: reconnects skip the full handshake
- Cache certificate chains already verified; changing the trusted roots
  clears that cache and all TLS sessions
- README: updated connection order; test: session resumption check and
  HandshakeProfile

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:16:09 +02:00

252 lines
9.4 KiB
Java

package com.minamp;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import android.app.Application;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.Handler;
import com.minamp.net.CertVerifier;
import com.minamp.net.Http;
import com.minamp.plex.Library;
import com.minamp.plex.Plex;
import com.minamp.plex.Server;
import com.minamp.util.Json;
public class App extends Application {
private static App instance;
public static App get() { return instance; }
public Handler handler;
public SharedPreferences prefs;
public Server server;
public Library library;
public ImageLoader images;
// quality: 0 = original (direct play when possible), otherwise MP3 kbps
public static final int[] QUALITIES = {0, 320, 256, 192, 128, 96};
public static final String[] QUALITY_LABELS = {"Original", "320 kbps MP3", "256 kbps MP3", "192 kbps MP3", "128 kbps MP3", "96 kbps MP3"};
@Override
public void onCreate() {
super.onCreate();
instance = this;
handler = new Handler();
prefs = getSharedPreferences("minamp", MODE_PRIVATE);
installCrashLogger();
// Android 2.2's HttpURLConnection pool is buggy; we use our own client, but be safe.
System.setProperty("http.keepAlive", "false");
String cid = prefs.getString("clientId", null);
if (cid == null) {
cid = UUID.randomUUID().toString();
prefs.edit().putString("clientId", cid).commit();
}
Plex.clientId = cid;
Plex.platformVersion = Build.VERSION.RELEASE;
Plex.device = Build.MODEL;
Plex.deviceName = Build.MODEL;
try { Plex.version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName; } catch (Exception ignored) {}
CertVerifier.setRootStore(new AssetRoots(this));
Library.preferredVariant = Math.max(0, Math.min(Library.VARIANTS - 1, prefs.getInt("tcVariant", 0)));
images = new ImageLoader(this);
Server.preferSecureAtHome = prefs.getBoolean("secureHome", false);
Server.saver = new Runnable() {
public void run() {
// remember which connection worked, so next start tries it first
final Server s = server;
if (s == null) return;
handler.post(new Runnable() {
public void run() {
if (server == s) prefs.edit().putString("server", s.toJson().toString()).commit();
}
});
}
};
loadServer();
PlayQueue.get().restore(this);
registerReceiver(new BroadcastReceiver() {
@Override public void onReceive(Context c, Intent i) {
// Network changed (Wi-Fi <-> mobile, new Wi-Fi...): re-pick the best connection lazily.
if (server != null) server.invalidate();
}
}, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
}
// ------------------------------------------------------------------ crash reports
private static final String CRASH_FILE = "crash.txt";
private void installCrashLogger() {
final Thread.UncaughtExceptionHandler previous = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
public void uncaughtException(Thread t, Throwable e) {
logError("crash in " + t.getName(), e);
if (previous != null) previous.uncaughtException(t, e);
}
});
}
/** Saves a stack trace to app storage (shown on next launch) and to the SD card. */
public static void logError(String where, Throwable e) {
App app = instance;
if (app == null) return;
try {
java.io.StringWriter sw = new java.io.StringWriter();
java.io.PrintWriter pw = new java.io.PrintWriter(sw);
pw.println("Minamp " + Plex.version + " on " + Build.MODEL + ", Android " + Build.VERSION.RELEASE);
pw.println(new java.util.Date() + " - " + where);
e.printStackTrace(pw);
pw.flush();
byte[] text = sw.toString().getBytes("UTF-8");
java.io.FileOutputStream out = app.openFileOutput(CRASH_FILE, MODE_PRIVATE);
out.write(text);
out.close();
try {
if (android.os.Environment.MEDIA_MOUNTED.equals(android.os.Environment.getExternalStorageState())) {
java.io.File f = new java.io.File(android.os.Environment.getExternalStorageDirectory(), "Minamp-error.txt");
java.io.FileOutputStream o2 = new java.io.FileOutputStream(f);
o2.write(text);
o2.close();
}
} catch (Throwable ignored) {}
} catch (Throwable ignored) {}
}
/** The last saved error report, or null. Reading it clears it. */
public String takeCrashReport() {
try {
java.io.File f = getFileStreamPath(CRASH_FILE);
if (!f.exists()) return null;
String s = new String(readAll(new java.io.FileInputStream(f)), "UTF-8");
f.delete();
return s;
} catch (Exception e) {
return null;
}
}
// ------------------------------------------------------------------ server/library
public void loadServer() {
String sj = prefs.getString("server", null);
String sec = prefs.getString("section", null);
server = null;
library = null;
if (sj != null) {
try {
server = Server.fromJson(Json.parseObj(sj));
if (sec != null) library = new Library(server, sec);
} catch (Exception e) {
server = null;
}
}
}
public void saveServer(Server s) {
prefs.edit().putString("server", s.toJson().toString()).remove("section").remove("sectionTitle").commit();
server = s;
library = null;
}
public void saveSection(String key, String title) {
prefs.edit().putString("section", key).putString("sectionTitle", title).commit();
library = new Library(server, key);
}
public boolean isConfigured() { return server != null && library != null; }
public String sectionTitle() { return prefs.getString("sectionTitle", "Music"); }
public void signOut() {
prefs.edit().remove("server").remove("section").remove("sectionTitle").remove("account").remove("username").commit();
server = null;
library = null;
Http.evictAll();
}
// ------------------------------------------------------------------ quality
public boolean onMobileData() {
try {
ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
NetworkInfo ni = cm.getActiveNetworkInfo();
return ni != null && ni.getType() != ConnectivityManager.TYPE_WIFI;
} catch (Exception e) {
return false;
}
}
public int qualityHome() { return prefs.getInt("qHome", 0); }
public int qualityAway() { return prefs.getInt("qAway", 192); }
/** Max kbps for the current situation; 0 = original. */
public int currentMaxKbps() {
boolean away = server == null || !server.isLocal() || onMobileData();
int q = away ? qualityAway() : qualityHome();
if (server != null && server.isRelay() && (q == 0 || q > 192)) q = 192; // relay is bandwidth-capped
return q;
}
public boolean downloadFirst() { return prefs.getBoolean("downloadFirst", false); }
// ------------------------------------------------------------------ trust store
/** Mozilla roots shipped as assets/roots/<sha1(subject)>-<n>.der */
static final class AssetRoots implements CertVerifier.RootStore {
private final Context ctx;
private Set<String> names;
AssetRoots(Context c) { ctx = c; }
public synchronized List<byte[]> find(byte[] subjectDer) {
try {
if (names == null) {
names = new HashSet<String>();
String[] l = ctx.getAssets().list("roots");
if (l != null) for (int i = 0; i < l.length; i++) names.add(l[i]);
}
String h = CertVerifier.sha1Hex(subjectDer);
List<byte[]> out = null;
for (int i = 0; names.contains(h + "-" + i + ".der"); i++) {
if (out == null) out = new ArrayList<byte[]>();
out.add(readAll(ctx.getAssets().open("roots/" + h + "-" + i + ".der")));
}
return out;
} catch (IOException e) {
return null;
}
}
}
static byte[] readAll(InputStream in) throws IOException {
try {
ByteArrayOutputStream b = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf)) > 0) b.write(buf, 0, n);
return b.toByteArray();
} finally {
in.close();
}
}
}