Private
Public Access
- 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>
252 lines
9.4 KiB
Java
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();
|
|
}
|
|
}
|
|
}
|