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/-.der */ static final class AssetRoots implements CertVerifier.RootStore { private final Context ctx; private Set names; AssetRoots(Context c) { ctx = c; } public synchronized List find(byte[] subjectDer) { try { if (names == null) { names = new HashSet(); 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 out = null; for (int i = 0; names.contains(h + "-" + i + ".der"); i++) { if (out == null) out = new ArrayList(); 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(); } } }