package net.i2p.android.router.util; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.preference.PreferenceManager; import android.text.TextUtils; import net.i2p.I2PAppContext; import net.i2p.android.preferences.GraphsPreferenceFragment; import net.i2p.android.router.I2PConstants; import net.i2p.android.router.R; import net.i2p.android.router.service.State; import net.i2p.data.DataHelper; import net.i2p.data.router.RouterAddress; import net.i2p.data.router.RouterInfo; import net.i2p.router.CommSystemFacade; import net.i2p.router.Router; import net.i2p.router.RouterContext; import net.i2p.router.transport.TransportManager; import net.i2p.router.transport.TransportUtil; import net.i2p.router.transport.udp.UDPTransport; import net.i2p.util.OrderedProperties; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; public abstract class Util implements I2PConstants { public static String getOurVersion(Context ctx) { PackageManager pm = ctx.getPackageManager(); String us = ctx.getPackageName(); try { PackageInfo pi = pm.getPackageInfo(us, 0); //System.err.println("VersionCode" + ": " + pi.versionCode); // http://doandroids.com/blogs/2010/6/10/android-classloader-dynamic-loading-of/ //_apkPath = pm.getApplicationInfo(us, 0).sourceDir; //System.err.println("APK Path" + ": " + _apkPath); if (pi.versionName != null) return pi.versionName; } catch (Exception e) { } return "??"; } /** * Get the active RouterContext. * * @return the active RouterContext, or null */ public static RouterContext getRouterContext() { List<RouterContext> contexts = RouterContext.listContexts(); if (!((contexts == null) || (contexts.isEmpty()))) { return contexts.get(0); } return null; } private static final String ANDROID_TAG = "I2P"; public static void e(String m) { e(m, null); } /** * Log to the context logger if available (which goes to the console buffer * and to logcat), else just to logcat. */ public static void e(String m, Throwable t) { I2PAppContext ctx = I2PAppContext.getCurrentContext(); if (ctx != null) ctx.logManager().getLog(Util.class).error(m, t); else if (android.util.Log.isLoggable(ANDROID_TAG, android.util.Log.ERROR)) { if (t != null) android.util.Log.e(ANDROID_TAG, m + ' ' + t + ' ' + android.util.Log.getStackTraceString(t)); else android.util.Log.e(ANDROID_TAG, m); } } public static void w(String m) { w(m, null); } public static void w(String m, Throwable t) { I2PAppContext ctx = I2PAppContext.getCurrentContext(); if (ctx != null) ctx.logManager().getLog(Util.class).warn(m, t); else if (android.util.Log.isLoggable(ANDROID_TAG, android.util.Log.WARN)) { if (t != null) android.util.Log.w(ANDROID_TAG, m + ' ' + t + ' ' + android.util.Log.getStackTraceString(t)); else android.util.Log.w(ANDROID_TAG, m); } } public static void i(String m) { i(m, null); } public static void i(String m, Throwable t) { I2PAppContext ctx = I2PAppContext.getCurrentContext(); if (ctx != null) ctx.logManager().getLog(Util.class).info(m, t); else if (android.util.Log.isLoggable(ANDROID_TAG, android.util.Log.INFO)) { if (t != null) android.util.Log.i(ANDROID_TAG, m + ' ' + t + ' ' + android.util.Log.getStackTraceString(t)); else android.util.Log.i(ANDROID_TAG, m); } } public static void d(String m) { d(m, null); } public static void d(String m, Throwable t) { I2PAppContext ctx = I2PAppContext.getCurrentContext(); if (ctx != null) ctx.logManager().getLog(Util.class).debug(m, t); else if (android.util.Log.isLoggable(ANDROID_TAG, android.util.Log.DEBUG)) { if (t != null) android.util.Log.d(ANDROID_TAG, m + ' ' + t + ' ' + android.util.Log.getStackTraceString(t)); else android.util.Log.d(ANDROID_TAG, m); } } /** * copied from various private components */ final static String PROP_I2NP_NTCP_PORT = "i2np.ntcp.port"; final static String PROP_I2NP_NTCP_AUTO_PORT = "i2np.ntcp.autoport"; public static List<Properties> getPropertiesFromPreferences(Context context) { List<Properties> pList = new ArrayList<>(); // Copy prefs Properties routerProps = new OrderedProperties(); // List to store stats for graphing List<String> statSummaries = new ArrayList<>(); // Properties to remove Properties toRemove = new OrderedProperties(); // List to store Log settings Properties logSettings = new OrderedProperties(); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); Map<String, ?> all = preferences.getAll(); // get values from the Map and make them strings. // This loop avoids needing to convert each one, or even know it's type, or if it exists yet. for (String x : all.keySet()) { if (x.startsWith("stat.summaries.")) { String stat = x.substring("stat.summaries.".length()); String checked = all.get(x).toString(); if (checked.equals("true")) { statSummaries.add(stat); } } else if (x.startsWith("logger.")) { logSettings.put(x, all.get(x).toString()); } else if ( x.equals("router.hiddenMode") || x.equals("i2cp.disableInterface")) { // special exception, we must invert the bool for these properties only. String string = all.get(x).toString(); String inverted = Boolean.toString(!Boolean.parseBoolean(string)); routerProps.setProperty(x, inverted); } else if (x.equals(context.getString(R.string.PREF_LANGUAGE))) { String language[] = TextUtils.split(all.get(x).toString(), "_"); if (language[0].equals(context.getString(R.string.DEFAULT_LANGUAGE))) { toRemove.setProperty("routerconsole.lang", ""); toRemove.setProperty("routerconsole.country", ""); } else { routerProps.setProperty("routerconsole.lang", language[0].toLowerCase()); if (language.length == 2) routerProps.setProperty("routerconsole.country", language[1].toUpperCase()); else toRemove.setProperty("routerconsole.country", ""); } } else if (!x.startsWith(ANDROID_PREF_PREFIX)) { // Skip over UI-related I2P Android settings String string = all.get(x).toString(); routerProps.setProperty(x, string); } } if (statSummaries.isEmpty()) { // If the graph preferences have not yet been seen, they should be the default if (preferences.getBoolean(GraphsPreferenceFragment.GRAPH_PREFERENCES_SEEN, false)) routerProps.setProperty("stat.summaries", ""); else toRemove.setProperty("stat.summaries", ""); } else { Iterator<String> iter = statSummaries.iterator(); StringBuilder buf = new StringBuilder(iter.next()); while (iter.hasNext()) { buf.append(",").append(iter.next()); } routerProps.setProperty("stat.summaries", buf.toString()); } // See net.i2p.router.web.ConfigNetHandler.saveChanges() int udpPort = Integer.parseInt(routerProps.getProperty(UDPTransport.PROP_INTERNAL_PORT, "-1")); if (udpPort <= 0) routerProps.remove(UDPTransport.PROP_INTERNAL_PORT); int ntcpPort = Integer.parseInt(routerProps.getProperty(PROP_I2NP_NTCP_PORT, "-1")); boolean ntcpAutoPort = Boolean.parseBoolean( routerProps.getProperty(PROP_I2NP_NTCP_AUTO_PORT, "true")); if (ntcpPort <= 0 || ntcpAutoPort) { routerProps.remove(PROP_I2NP_NTCP_PORT); toRemove.setProperty(PROP_I2NP_NTCP_PORT, ""); } pList.add(routerProps); pList.add(toRemove); pList.add(logSettings); return pList; } // propName -> defaultValue private static HashMap<String, Boolean> booleanOptionsRequiringRestart = new HashMap<>(); private static HashMap<String, String> stringOptionsRequiringRestart = new HashMap<>(); static { HashMap<String, Boolean> boolToAdd = new HashMap<>(); HashMap<String, String> strToAdd = new HashMap<>(); boolToAdd.put(TransportManager.PROP_ENABLE_UPNP, true); boolToAdd.put(TransportManager.PROP_ENABLE_NTCP, true); boolToAdd.put(TransportManager.PROP_ENABLE_UDP, true); boolToAdd.put(PROP_I2NP_NTCP_AUTO_PORT, true); boolToAdd.put(Router.PROP_HIDDEN, false); strToAdd.put(UDPTransport.PROP_INTERNAL_PORT, "-1"); strToAdd.put(PROP_I2NP_NTCP_PORT, "-1"); booleanOptionsRequiringRestart.putAll(boolToAdd); stringOptionsRequiringRestart.putAll(strToAdd); } /** * This function performs two tasks: * <ul><li> * The Properties object is modified to ensure that all options are valid * for the current state of the Android device (e.g. what type of network * the device is connected to). * </li><li> * The Properties object is checked to determine whether any options have * changed that will require a router restart. * </li></ul> * * @param props a Properties object containing the router.config * @param toRemove a Collection of properties that will be removed * @return true if the router needs to be restarted. */ public static boolean checkAndCorrectRouterConfig(Context context, Properties props, Collection<String> toRemove) { // Disable UPnP on mobile networks, ignoring user's configuration // TODO disabled until changes elsewhere are finished //if (Connectivity.isConnectedMobile(context)) { // props.setProperty(TransportManager.PROP_ENABLE_UPNP, Boolean.toString(false)); //} // Now check if a restart is required boolean restartRequired = false; RouterContext rCtx = getRouterContext(); if (rCtx != null) { for (Map.Entry<String, Boolean> option : booleanOptionsRequiringRestart.entrySet()) { String propName = option.getKey(); boolean defaultValue = option.getValue(); boolean currentValue = defaultValue ? rCtx.getBooleanPropertyDefaultTrue(propName) : rCtx.getBooleanProperty(propName); boolean newValue = Boolean.parseBoolean(props.getProperty(propName, Boolean.toString(defaultValue))); restartRequired |= (currentValue != newValue); } if (!restartRequired) { // Cut out now if we already know the answer for (Map.Entry<String, String> option : stringOptionsRequiringRestart.entrySet()) { String propName = option.getKey(); String defaultValue = option.getValue(); String currentValue = rCtx.getProperty(propName, defaultValue); String newValue = props.getProperty(propName, defaultValue); restartRequired |= !currentValue.equals(newValue); } } } return restartRequired; } public static String getFileDir(Context context) { // This needs to be changed so that we can have an alternative place return context.getFilesDir().getAbsolutePath(); } /** * Write properties to a file. If the file does not exist, it is created. * If the properties already exist in the file, they are updated. * * @param dir the file directory * @param file relative to dir * @param props properties to set */ public static void writePropertiesToFile(Context ctx, String dir, String file, Properties props) { mergeResourceToFile(ctx, dir, file, 0, props, null); } /** * Load defaults from resource, then add props from settings, and write back. * If resID is 0, defaults are not written over the existing file content. * * @param dir the file directory * @param file relative to dir * @param resID the ID of the default resource, or 0 * @param userProps local properties or null * @param toRemove properties to remove, or null */ public static void mergeResourceToFile(Context ctx, String dir, String file, int resID, Properties userProps, Collection<String> toRemove) { InputStream fin = null; InputStream in = null; try { Properties props = new OrderedProperties(); try { fin = new FileInputStream(new File(dir, file)); DataHelper.loadProps(props, fin); if (resID > 0) Util.d("Merging resource into file " + file); else Util.d("Merging properties into file " + file); } catch (IOException ioe) { if (resID > 0) Util.d("Creating file " + file + " from resource"); else Util.d("Creating file " + file + " from properties"); } // write in default settings if (resID > 0) in = ctx.getResources().openRawResource(resID); if (in != null) DataHelper.loadProps(props, in); // override with user settings if (userProps != null) props.putAll(userProps); if (toRemove != null) { for (String key : toRemove) { props.remove(key); } } File path = new File(dir, file); DataHelper.storeProps(props, path); Util.d("Saved " + props.size() + " properties in " + file); } catch (IOException ioe) { } catch (Resources.NotFoundException nfe) { } finally { if (in != null) try { in.close(); } catch (IOException ioe) { } if (fin != null) try { fin.close(); } catch (IOException ioe) { } } } public static boolean isStopping(State state) { return state == State.STOPPING || state == State.MANUAL_STOPPING || state == State.MANUAL_QUITTING; } public static boolean isStopped(State state) { return state == State.STOPPED || state == State.MANUAL_STOPPED || state == State.MANUAL_QUITTED || state == State.WAITING; } public static class NetStatus { public enum Level { ERROR, WARN, INFO, } public final Level level; public final String status; public NetStatus(Level level, String status) { this.level = level; this.status = status; } } public static NetStatus getNetStatus(Context ctx, RouterContext rCtx) { if (rCtx.commSystem().isDummy()) return new NetStatus(NetStatus.Level.INFO, ctx.getString(R.string.vm_comm_system)); if (rCtx.router().getUptime() > 60 * 1000 && (!rCtx.router().gracefulShutdownInProgress()) && !rCtx.clientManager().isAlive()) // not a router problem but the user should know return new NetStatus(NetStatus.Level.ERROR, ctx.getString(R.string.net_status_error_i2cp)); // Warn based on actual skew from peers, not update status, so if we successfully offset // the clock, we don't complain. //if (!rCtx.clock().getUpdatedSuccessfully()) long skew = rCtx.commSystem().getFramedAveragePeerClockSkew(33); // Display the actual skew, not the offset if (Math.abs(skew) > 30 * 1000) return new NetStatus(NetStatus.Level.ERROR, ctx.getString(R.string.net_status_error_skew, DataHelper.formatDuration2(Math.abs(skew)) .replace("−", "-") .replace(" ", " "))); if (rCtx.router().isHidden()) return new NetStatus(NetStatus.Level.INFO, ctx.getString(R.string.hidden)); RouterInfo routerInfo = rCtx.router().getRouterInfo(); if (routerInfo == null) return new NetStatus(NetStatus.Level.INFO, ctx.getString(R.string.testing)); CommSystemFacade.Status status = rCtx.commSystem().getStatus(); switch (status) { case OK: case IPV4_OK_IPV6_UNKNOWN: case IPV4_OK_IPV6_FIREWALLED: case IPV4_UNKNOWN_IPV6_OK: case IPV4_DISABLED_IPV6_OK: case IPV4_SNAT_IPV6_OK: RouterAddress ra = routerInfo.getTargetAddress("NTCP"); if (ra == null) return new NetStatus(NetStatus.Level.INFO, toStatusString(ctx, status)); byte[] ip = ra.getIP(); if (ip == null) return new NetStatus(NetStatus.Level.ERROR, ctx.getString(R.string.net_status_error_unresolved_tcp)); // TODO set IPv6 arg based on configuration? if (TransportUtil.isPubliclyRoutable(ip, true)) return new NetStatus(NetStatus.Level.INFO, toStatusString(ctx, status)); return new NetStatus(NetStatus.Level.ERROR, ctx.getString(R.string.net_status_error_private_tcp)); case IPV4_SNAT_IPV6_UNKNOWN: case DIFFERENT: return new NetStatus(NetStatus.Level.ERROR, ctx.getString(R.string.symmetric_nat)); case REJECT_UNSOLICITED: case IPV4_DISABLED_IPV6_FIREWALLED: if (routerInfo.getTargetAddress("NTCP") != null) return new NetStatus(NetStatus.Level.WARN, ctx.getString(R.string.net_status_warn_firewalled_inbound_tcp)); // fall through... case IPV4_FIREWALLED_IPV6_OK: case IPV4_FIREWALLED_IPV6_UNKNOWN: if (rCtx.netDb().floodfillEnabled()) return new NetStatus(NetStatus.Level.WARN, ctx.getString(R.string.net_status_warn_firewalled_floodfill)); //if (rCtx.router().getRouterInfo().getCapabilities().indexOf('O') >= 0) // return _("WARN-Firewalled and Fast"); return new NetStatus(NetStatus.Level.INFO, toStatusString(ctx, status)); case DISCONNECTED: return new NetStatus(NetStatus.Level.INFO, ctx.getString(R.string.net_status_info_disconnected)); case HOSED: return new NetStatus(NetStatus.Level.ERROR, ctx.getString(R.string.net_status_error_udp_port)); case UNKNOWN: case IPV4_UNKNOWN_IPV6_FIREWALLED: case IPV4_DISABLED_IPV6_UNKNOWN: default: ra = routerInfo.getTargetAddress("SSU"); if (ra == null && rCtx.router().getUptime() > 5 * 60 * 1000) { if (rCtx.commSystem().countActivePeers() <= 0) return new NetStatus(NetStatus.Level.ERROR, ctx.getString(R.string.net_status_error_no_active_peers)); else if (rCtx.getProperty(ctx.getString(R.string.PROP_I2NP_NTCP_HOSTNAME)) == null || rCtx.getProperty(ctx.getString(R.string.PROP_I2NP_NTCP_PORT)) == null) return new NetStatus(NetStatus.Level.ERROR, ctx.getString(R.string.net_status_error_udp_disabled_tcp_not_set)); else return new NetStatus(NetStatus.Level.WARN, ctx.getString(R.string.net_status_warn_firewalled_udp_disabled)); } return new NetStatus(NetStatus.Level.INFO, toStatusString(ctx, status)); } } private static String toStatusString(Context ctx, CommSystemFacade.Status status) { String ipv4Status = ""; String ipv6Status = ""; switch (status) { case OK: return ctx.getString(android.R.string.ok); case IPV4_OK_IPV6_UNKNOWN: ipv4Status = ctx.getString(android.R.string.ok); ipv6Status = ctx.getString(R.string.testing); break; case IPV4_OK_IPV6_FIREWALLED: ipv4Status = ctx.getString(android.R.string.ok); ipv6Status = ctx.getString(R.string.firewalled); break; case IPV4_UNKNOWN_IPV6_OK: ipv4Status = ctx.getString(R.string.testing); ipv6Status = ctx.getString(android.R.string.ok); break; case IPV4_FIREWALLED_IPV6_OK: ipv4Status = ctx.getString(R.string.firewalled); ipv6Status = ctx.getString(android.R.string.ok); break; case IPV4_DISABLED_IPV6_OK: ipv4Status = ctx.getString(R.string.disabled); ipv6Status = ctx.getString(android.R.string.ok); break; case IPV4_SNAT_IPV6_OK: ipv4Status = ctx.getString(R.string.symmetric_nat); ipv6Status = ctx.getString(android.R.string.ok); break; case DIFFERENT: return ctx.getString(R.string.symmetric_nat); case IPV4_SNAT_IPV6_UNKNOWN: ipv4Status = ctx.getString(R.string.symmetric_nat); ipv6Status = ctx.getString(R.string.testing); break; case IPV4_FIREWALLED_IPV6_UNKNOWN: ipv4Status = ctx.getString(R.string.firewalled); ipv6Status = ctx.getString(R.string.testing); break; case REJECT_UNSOLICITED: return ctx.getString(R.string.firewalled); case IPV4_UNKNOWN_IPV6_FIREWALLED: ipv4Status = ctx.getString(R.string.testing); ipv6Status = ctx.getString(R.string.firewalled); break; case IPV4_DISABLED_IPV6_UNKNOWN: ipv4Status = ctx.getString(R.string.disabled); ipv6Status = ctx.getString(R.string.testing); break; case IPV4_DISABLED_IPV6_FIREWALLED: ipv4Status = ctx.getString(R.string.disabled); ipv6Status = ctx.getString(R.string.firewalled); break; case UNKNOWN: return ctx.getString(R.string.testing); default: return status.toStatusString(); } return ctx.getString(R.string.net_status_ipv4_ipv6, ipv4Status, ipv6Status); } public static String formatSize(double size) { return formatSize(size, 0); } public static String formatSpeed(double size) { return formatSize(size, 1); } public static String formatSize(double size, int baseScale) { int scale; for (int i = 0; i < baseScale; i++) { size /= 1024.0D; } for (scale = baseScale; size >= 1024.0D; size /= 1024.0D) { ++scale; } // control total width DecimalFormat fmt; if (size >= 1000) { fmt = new DecimalFormat("#0"); } else if (size >= 100) { fmt = new DecimalFormat("#0.0"); } else { fmt = new DecimalFormat("#0.00"); } String str = fmt.format(size); switch (scale) { case 1: return str + "K"; case 2: return str + "M"; case 3: return str + "G"; case 4: return str + "T"; case 5: return str + "P"; case 6: return str + "E"; case 7: return str + "Z"; case 8: return str + "Y"; default: return str + ""; } } }