package net.i2p.android.router; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Typeface; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.AlertDialog; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TableLayout; import android.widget.TableRow; import android.widget.TextView; import android.widget.ToggleButton; import net.i2p.android.I2PActivityBase; import net.i2p.android.help.BrowserConfigActivity; import net.i2p.android.router.dialog.FirstStartDialog; import net.i2p.android.router.service.RouterService; import net.i2p.android.router.service.State; import net.i2p.android.router.util.Connectivity; import net.i2p.android.router.util.LongToggleButton; import net.i2p.android.router.util.Util; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; import net.i2p.router.RouterContext; import net.i2p.router.TunnelPoolSettings; import java.text.Collator; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; public class MainFragment extends I2PFragmentBase { private Handler _handler; private Runnable _updater; private Runnable _oneShotUpdate; private String _savedStatus; private ImageView mConsoleLights; private LongToggleButton mOnOffButton; private LinearLayout vGracefulButtons; private ScrollView mScrollView; private View vStatusContainer; private ImageView vNetStatusLevel; private TextView vNetStatusText; private View vNonNetStatus; private TextView vUptime; private TextView vActive; private TextView vKnown; private TableLayout vTunnels; private LinearLayout vAdvStatus; private TextView vAdvStatusText; private static final String PREF_CONFIGURE_BROWSER = "app.dialog.configureBrowser"; private static final String PREF_FIRST_START = "app.router.firstStart"; private static final String PREF_SHOW_STATS = "i2pandroid.main.showStats"; protected static final String PROP_NEW_INSTALL = "i2p.newInstall"; protected static final String PROP_NEW_VERSION = "i2p.newVersion"; RouterControlListener mCallback; // Container Activity must implement this interface public interface RouterControlListener { boolean shouldShowOnOff(); boolean shouldBeOn(); void onStartRouterClicked(); boolean onStopRouterClicked(); /** * @since 0.9.19 */ boolean isGracefulShutdownInProgress(); /** * @since 0.9.19 */ boolean onGracefulShutdownClicked(); /** * @since 0.9.19 */ boolean onCancelGracefulShutdownClicked(); } @Override public void onAttach(Activity activity) { super.onAttach(activity); // This makes sure that the container activity has implemented // the callback interface. If not, it throws an exception try { mCallback = (RouterControlListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement RouterControlListener"); } } /** * Called when the fragment is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Init stuff here so settings work. if (savedInstanceState != null) { lastRouterState = savedInstanceState.getParcelable("lastState"); String saved = savedInstanceState.getString("status"); if (saved != null) { _savedStatus = saved; } } _handler = new Handler(); _updater = new Updater(); _oneShotUpdate = new OneShotUpdate(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_main, container, false); mConsoleLights = (ImageView) v.findViewById(R.id.console_lights); mOnOffButton = (LongToggleButton) v.findViewById(R.id.router_onoff_button); vGracefulButtons = (LinearLayout) v.findViewById(R.id.router_graceful_buttons); mScrollView = (ScrollView) v.findViewById(R.id.main_scrollview); vStatusContainer = v.findViewById(R.id.status_container); vNetStatusLevel = (ImageView) v.findViewById(R.id.console_net_status_level); vNetStatusText = (TextView) v.findViewById(R.id.console_net_status_text); vNonNetStatus = v.findViewById(R.id.console_non_net_status_container); vUptime = (TextView) v.findViewById(R.id.console_uptime); vActive = (TextView) v.findViewById(R.id.console_active); vKnown = (TextView) v.findViewById(R.id.console_known); vTunnels = (TableLayout) v.findViewById(R.id.main_tunnels); vAdvStatus = (LinearLayout) v.findViewById(R.id.console_advanced_status); vAdvStatusText = (TextView) v.findViewById(R.id.console_advanced_status_text); updateState(lastRouterState); mOnOffButton.setOnLongClickListener(new View.OnLongClickListener() { public boolean onLongClick(View view) { boolean on = ((ToggleButton) view).isChecked(); if (on) { mCallback.onStartRouterClicked(); updateOneShot(); checkFirstStart(); } else if (mCallback.onGracefulShutdownClicked()) updateOneShot(); return true; } }); Button gb = (Button) v.findViewById(R.id.button_shutdown_now); gb.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { if (mCallback.isGracefulShutdownInProgress()) if (mCallback.onStopRouterClicked()) updateOneShot(); return true; } }); gb = (Button) v.findViewById(R.id.button_cancel_graceful); gb.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { if (mCallback.isGracefulShutdownInProgress()) if (mCallback.onCancelGracefulShutdownClicked()) updateOneShot(); return true; } }); return v; } @Override public void onStart() { super.onStart(); _handler.removeCallbacks(_updater); _handler.removeCallbacks(_oneShotUpdate); if (_savedStatus != null) { TextView tv = (TextView) getActivity().findViewById(R.id.console_advanced_status_text); tv.setText(_savedStatus); } checkDialog(); _handler.postDelayed(_updater, 100); LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(getActivity()); IntentFilter filter = new IntentFilter(); filter.addAction(RouterService.LOCAL_BROADCAST_STATE_NOTIFICATION); filter.addAction(RouterService.LOCAL_BROADCAST_STATE_CHANGED); lbm.registerReceiver(onStateChange, filter); lbm.sendBroadcast(new Intent(RouterService.LOCAL_BROADCAST_REQUEST_STATE)); } private State lastRouterState; private BroadcastReceiver onStateChange = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { State state = intent.getParcelableExtra(RouterService.LOCAL_BROADCAST_EXTRA_STATE); if (lastRouterState == null || lastRouterState != state) { updateState(state); // If we have stopped, clear the status info immediately if (Util.isStopped(state)) { updateOneShot(); } lastRouterState = state; } } }; @Override public void onStop() { super.onStop(); _handler.removeCallbacks(_updater); _handler.removeCallbacks(_oneShotUpdate); LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(onStateChange); } @Override public void onResume() { super.onResume(); updateOneShot(); } @Override public void onSaveInstanceState(Bundle outState) { if (lastRouterState != null) outState.putParcelable("lastState", lastRouterState); if (_savedStatus != null) outState.putString("status", _savedStatus); super.onSaveInstanceState(outState); } private void updateOneShot() { _handler.postDelayed(_oneShotUpdate, 10); } private class OneShotUpdate implements Runnable { public void run() { updateVisibility(); try { updateStatus(); } catch (NullPointerException npe) { // RouterContext wasn't quite ready Util.w("Status was updated before RouterContext was ready", npe); } } } private class Updater implements Runnable { private int counter; private final int delay = 1000; private final int toloop = delay / 500; public void run() { updateVisibility(); if (counter++ % toloop == 0) { try { updateStatus(); } catch (NullPointerException npe) { // RouterContext wasn't quite ready Util.w("Status was updated before RouterContext was ready", npe); } } //_handler.postDelayed(this, 2500); _handler.postDelayed(this, delay); } } private void updateVisibility() { boolean showOnOff = mCallback.shouldShowOnOff(); mOnOffButton.setVisibility(showOnOff ? View.VISIBLE : View.GONE); boolean isOn = mCallback.shouldBeOn(); mOnOffButton.setChecked(isOn); boolean isGraceful = mCallback.isGracefulShutdownInProgress(); vGracefulButtons.setVisibility(isGraceful ? View.VISIBLE : View.GONE); if (isOn && isGraceful) { RouterContext ctx = getRouterContext(); if (ctx != null) { TextView tv = (TextView) vGracefulButtons.findViewById(R.id.router_graceful_status); long ms = ctx.router().getShutdownTimeRemaining(); if (ms > 1000) { tv.setText(getActivity().getResources().getString(R.string.button_router_graceful, DataHelper.formatDuration(ms))); } else { tv.setText(getActivity().getString(R.string.notification_status_stopping)); } } } } public void updateState(State newState) { if (newState == State.INIT || newState == State.STOPPED || newState == State.MANUAL_STOPPED || newState == State.MANUAL_QUITTED || newState == State.NETWORK_STOPPED) { mConsoleLights.setImageResource(R.drawable.routerlogo_0); } else if (newState == State.STARTING || //newState == State.GRACEFUL_SHUTDOWN || // Don't change lights for graceful newState == State.STOPPING || newState == State.MANUAL_STOPPING || newState == State.MANUAL_QUITTING || newState == State.NETWORK_STOPPING) { mConsoleLights.setImageResource(R.drawable.routerlogo_1); } else if (newState == State.RUNNING) { mConsoleLights.setImageResource(R.drawable.routerlogo_2); } else if (newState == State.ACTIVE) { mConsoleLights.setImageResource(R.drawable.routerlogo_3); } else if (newState == State.WAITING) { mConsoleLights.setImageResource(R.drawable.routerlogo_4); } // Ignore unknown states. } private void updateStatus() { RouterContext ctx = getRouterContext(); if (!Connectivity.isConnected(getActivity())) { // Manually set state, RouterService won't be running updateState(State.WAITING); vNetStatusText.setText(R.string.no_internet); vStatusContainer.setVisibility(View.VISIBLE); vNonNetStatus.setVisibility(View.GONE); } else if (lastRouterState != null && !Util.isStopping(lastRouterState) && !Util.isStopped(lastRouterState) && ctx != null) { Util.NetStatus netStatus = Util.getNetStatus(getActivity(), ctx); switch (netStatus.level) { case ERROR: vNetStatusLevel.setImageDrawable(getResources().getDrawable(R.drawable.ic_error_red_24dp)); vNetStatusLevel.setVisibility(View.VISIBLE); break; case WARN: vNetStatusLevel.setImageDrawable(getResources().getDrawable(R.drawable.ic_warning_amber_24dp)); vNetStatusLevel.setVisibility(View.VISIBLE); break; case INFO: default: vNetStatusLevel.setVisibility(View.GONE); } vNetStatusText.setText(getString(R.string.settings_label_network) + ": " + netStatus.status); String uptime = DataHelper.formatDuration(ctx.router().getUptime()); int active = ctx.commSystem().countActivePeers(); int known = Math.max(ctx.netDb().getKnownRouters() - 1, 0); vUptime.setText("" + uptime); vActive.setText("" + active); vKnown.setText("" + known); // Load running tunnels loadDestinations(ctx); if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean(PREF_SHOW_STATS, false)) { int inEx = ctx.tunnelManager().getFreeTunnelCount(); int outEx = ctx.tunnelManager().getOutboundTunnelCount(); int inCl = ctx.tunnelManager().getInboundClientTunnelCount(); int outCl = ctx.tunnelManager().getOutboundClientTunnelCount(); int part = ctx.tunnelManager().getParticipatingCount(); double dLag = ctx.statManager().getRate("jobQueue.jobLag").getRate(60000).getAverageValue(); String jobLag = DataHelper.formatDuration((long) dLag); String msgDelay = DataHelper.formatDuration(ctx.throttle().getMessageDelay()); String tunnelStatus = ctx.throttle().getTunnelStatus(); //ctx.commSystem().getReachabilityStatus(); String status = "Exploratory Tunnels in/out: " + inEx + " / " + outEx + "\nClient Tunnels in/out: " + inCl + " / " + outCl; // Need to see if we have the participation option set to on. // I thought there was a router method for that? I guess not! WHY NOT? // It would be easier if we had a number to test status. String participate = "\nParticipation: " + tunnelStatus + " (" + part + ")"; String details = "\nMemory: " + DataHelper.formatSize(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) + "B / " + DataHelper.formatSize(Runtime.getRuntime().maxMemory()) + 'B' + "\nJob Lag: " + jobLag + "\nMsg Delay: " + msgDelay; _savedStatus = status + participate + details; vAdvStatusText.setText(_savedStatus); vAdvStatus.setVisibility(View.VISIBLE); } else { vAdvStatus.setVisibility(View.GONE); } vStatusContainer.setVisibility(View.VISIBLE); vNonNetStatus.setVisibility(View.VISIBLE); // Usage stats in bottom toolbar double inBw = ctx.bandwidthLimiter().getReceiveBps(); double outBw = ctx.bandwidthLimiter().getSendBps(); double inData = ctx.bandwidthLimiter().getTotalAllocatedInboundBytes(); double outData = ctx.bandwidthLimiter().getTotalAllocatedOutboundBytes(); ((TextView) getActivity().findViewById(R.id.console_download_stats)).setText( Util.formatSpeed(inBw) + "Bps / " + Util.formatSize(inData) + "B"); ((TextView) getActivity().findViewById(R.id.console_upload_stats)).setText( Util.formatSpeed(outBw) + "Bps / " + Util.formatSize(outData) + "B"); getActivity().findViewById(R.id.console_usage_stats).setVisibility(View.VISIBLE); } else { // network but no router context vStatusContainer.setVisibility(View.GONE); getActivity().findViewById(R.id.console_usage_stats).setVisibility(View.INVISIBLE); /** * ** * RouterService svc = _routerService; String status = "connected? " * + Util.isConnected(this) + "\nMemory: " + * DataHelper.formatSize(Runtime.getRuntime().totalMemory() - * Runtime.getRuntime().freeMemory()) + "B / " + * DataHelper.formatSize(Runtime.getRuntime().maxMemory()) + 'B' + * "\nhave ctx? " + (ctx != null) + "\nhave svc? " + (svc != null) + * "\nis bound? " + _isBound + "\nsvc state: " + (svc == null ? * "null" : svc.getState()) + "\ncan start? " + (svc == null ? * "null" : svc.canManualStart()) + "\ncan stop? " + (svc == null ? * "null" : svc.canManualStop()); tv.setText(status); * tv.setVisibility(View.VISIBLE); *** */ } } /** * Based on net.i2p.router.web.SummaryHelper.getDestinations() * * @param ctx The RouterContext */ private void loadDestinations(RouterContext ctx) { vTunnels.removeAllViews(); List<Destination> clients = null; if (ctx.clientManager() != null) clients = new ArrayList<Destination>(ctx.clientManager().listClients()); if (clients != null && !clients.isEmpty()) { Collections.sort(clients, new AlphaComparator(ctx)); for (Destination client : clients) { String name = getName(ctx, client); Hash h = client.calculateHash(); TableRow dest = new TableRow(getActivity()); dest.setPadding(16, 4, 0, 4); // Client or server TextView type = new TextView(getActivity()); type.setTextColor(getResources().getColor(android.R.color.primary_text_light)); type.setTypeface(Typeface.DEFAULT_BOLD); type.setGravity(Gravity.CENTER); if (ctx.clientManager().shouldPublishLeaseSet(h)) type.setText(R.string.char_server_tunnel); else type.setText(R.string.char_client_tunnel); dest.addView(type); // Name TextView destName = new TextView(getActivity()); destName.setPadding(16, 0, 0, 0); destName.setGravity(Gravity.CENTER_VERTICAL); destName.setText(name); dest.addView(destName); // Status LeaseSet ls = ctx.netDb().lookupLeaseSetLocally(h); if (ls != null && ctx.tunnelManager().getOutboundClientTunnelCount(h) > 0) { long timeToExpire = ls.getEarliestLeaseDate() - ctx.clock().now(); if (timeToExpire < 0) { // red or yellow light type.setBackgroundResource(R.drawable.tunnel_yellow); } else { // green light type.setBackgroundResource(R.drawable.tunnel_green); } } else { // yellow light type.setBackgroundResource(R.drawable.tunnel_yellow); } vTunnels.addView(dest); } } else { TableRow empty = new TableRow(getActivity()); TextView emptyText = new TextView(getActivity()); emptyText.setText(R.string.no_tunnels_running); empty.addView(emptyText); vTunnels.addView(empty); } } private static final String SHARED_CLIENTS = "shared clients"; /** * compare translated nicknames - put "shared clients" first in the sort */ private class AlphaComparator implements Comparator<Destination> { private String xsc; private RouterContext _ctx; public AlphaComparator(RouterContext ctx) { _ctx = ctx; xsc = _(ctx, SHARED_CLIENTS); } public int compare(Destination lhs, Destination rhs) { String lname = getName(_ctx, lhs); String rname = getName(_ctx, rhs); if (lname.equals(xsc)) return -1; if (rname.equals(xsc)) return 1; return Collator.getInstance().compare(lname, rname); } } /** * translate here so collation works above */ private String getName(RouterContext ctx, Destination d) { TunnelPoolSettings in = ctx.tunnelManager().getInboundSettings(d.calculateHash()); String name = (in != null ? in.getDestinationNickname() : null); if (name == null) { TunnelPoolSettings out = ctx.tunnelManager().getOutboundSettings(d.calculateHash()); name = (out != null ? out.getDestinationNickname() : null); } if (name == null) name = d.calculateHash().toBase64().substring(0, 6); else name = _(ctx, name); return name; } private String _(RouterContext ctx, String s) { if (SHARED_CLIENTS.equals(s)) return getString(R.string.shared_clients); else return s; } private void checkDialog() { final I2PActivityBase ab = (I2PActivityBase) getActivity(); String language = PreferenceManager.getDefaultSharedPreferences(ab).getString( getString(R.string.PREF_LANGUAGE), null ); if (language == null) { AlertDialog.Builder b = new AlertDialog.Builder(getActivity()); b.setTitle(R.string.choose_language) .setItems(R.array.language_names, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Save the language choice String language = getResources().getStringArray(R.array.languages)[which]; PreferenceManager.getDefaultSharedPreferences(getActivity()) .edit() .putString(getString(R.string.PREF_LANGUAGE), language) .commit(); // Close the dialog dialog.dismiss(); // Broadcast the change to RouterService just in case the router is running Intent intent = new Intent(RouterService.LOCAL_BROADCAST_LOCALE_CHANGED); LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(intent); // Update the parent ab.notifyLocaleChanged(); // Run checkDialog() again to show the next dialog // (if the change doesn't restart the Activity) checkDialog(); } }) .setCancelable(false) .show(); } else if (ab.getPref(PREF_CONFIGURE_BROWSER, true)) { AlertDialog.Builder b = new AlertDialog.Builder(getActivity()); b.setTitle(R.string.configure_browser_title) .setMessage(R.string.configure_browser_for_i2p) .setCancelable(false) .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int i) { dialog.dismiss(); ab.setPref(PREF_CONFIGURE_BROWSER, false); Intent hi = new Intent(getActivity(), BrowserConfigActivity.class); startActivity(hi); } }) .setNegativeButton(R.string.no, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int i) { dialog.cancel(); ab.setPref(PREF_CONFIGURE_BROWSER, false); } }) .show(); } /*VersionDialog dialog = new VersionDialog(); String oldVersion = ((I2PActivityBase) getActivity()).getPref(PREF_INSTALLED_VERSION, "??"); if(oldVersion.equals("??")) { // TODO Don't show this dialog until it is reworked Bundle args = new Bundle(); args.putInt(VersionDialog.DIALOG_TYPE, VersionDialog.DIALOG_NEW_INSTALL); dialog.setArguments(args); dialog.show(getActivity().getSupportFragmentManager(), "newinstall"); } else { // TODO Don't show dialog on new version until we have something new to tell them String currentVersion = Util.getOurVersion(getActivity()); if(!oldVersion.equals(currentVersion)) { Bundle args = new Bundle(); args.putInt(VersionDialog.DIALOG_TYPE, VersionDialog.DIALOG_NEW_VERSION); dialog.setArguments(args); dialog.show(getActivity().getSupportFragmentManager(), "newversion"); } }*/ } private void checkFirstStart() { I2PActivityBase ab = (I2PActivityBase) getActivity(); boolean firstStart = ab.getPref(PREF_FIRST_START, true); if (firstStart) { FirstStartDialog dialog = new FirstStartDialog(); dialog.show(getActivity().getSupportFragmentManager(), "firststart"); ab.setPref(PREF_FIRST_START, false); } } }