package net.i2p.android.router.service;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.support.v4.content.LocalBroadcastManager;
import net.i2p.android.router.R;
import net.i2p.android.router.receiver.I2PReceiver;
import net.i2p.android.router.util.Connectivity;
import net.i2p.android.router.util.Notifications;
import net.i2p.android.router.util.Util;
import net.i2p.android.util.LocaleManager;
import net.i2p.data.DataHelper;
import net.i2p.router.Job;
import net.i2p.router.Router;
import net.i2p.router.RouterContext;
import net.i2p.router.RouterLaunch;
import java.lang.ref.WeakReference;
/**
* Runs the router
*/
public class RouterService extends Service {
/**
* A request to this service for the current router state. Broadcasting
* this will trigger a state notification.
*/
public static final String LOCAL_BROADCAST_REQUEST_STATE = "net.i2p.android.LOCAL_BROADCAST_REQUEST_STATE";
/**
* A notification of the current state. This is informational; the state
* has not changed.
*/
public static final String LOCAL_BROADCAST_STATE_NOTIFICATION = "net.i2p.android.LOCAL_BROADCAST_STATE_NOTIFICATION";
/**
* The state has just changed.
*/
public static final String LOCAL_BROADCAST_STATE_CHANGED = "net.i2p.android.LOCAL_BROADCAST_STATE_CHANGED";
public static final String LOCAL_BROADCAST_EXTRA_STATE = "net.i2p.android.STATE";
/**
* The locale has just changed.
*/
public static final String LOCAL_BROADCAST_LOCALE_CHANGED = "net.i2p.android.LOCAL_BROADCAST_LOCALE_CHANGED";
private LocaleManager localeManager = new LocaleManager();
private RouterContext _context;
private String _myDir;
//private String _apkPath;
private State _state = State.INIT;
private Thread _starterThread;
private StatusBar _statusBar;
private Notifications _notif;
private I2PReceiver _receiver;
private IBinder _binder;
private final Object _stateLock = new Object();
private Handler _handler;
private Runnable _updater;
private static final String SHARED_PREFS = "net.i2p.android.router";
private static final String LAST_STATE = "service.lastState";
private static final String EXTRA_RESTART = "restart";
private static final String MARKER = "************************************** ";
/**
* This is a list of callbacks that have been registered with the
* service. Note that this is package scoped (instead of private) so
* that it can be accessed more efficiently from inner classes.
*/
final RemoteCallbackList<IRouterStateCallback> mStateCallbacks
= new RemoteCallbackList<>();
@Override
public void onCreate() {
State lastState = getSavedState();
setState(State.INIT);
Util.d(this + " onCreate called"
+ " Saved state is: " + lastState
+ " Current state is: " + _state);
//(new File(getFilesDir(), "wrapper.log")).delete();
_myDir = getFilesDir().getAbsolutePath();
// init other stuff here, delete log, etc.
Init init = new Init(this);
init.initialize();
//_apkPath = init.getAPKPath();
_statusBar = new StatusBar(this);
// Remove stale notification icon.
_statusBar.remove();
_notif = new Notifications(this);
_binder = new RouterBinder(this);
_handler = new Handler();
_updater = new Updater();
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
lbm.registerReceiver(onStateRequested, new IntentFilter(LOCAL_BROADCAST_REQUEST_STATE));
lbm.registerReceiver(onLocaleChanged, new IntentFilter(LOCAL_BROADCAST_LOCALE_CHANGED));
if(lastState == State.RUNNING || lastState == State.ACTIVE) {
Intent intent = new Intent(this, RouterService.class);
intent.putExtra(EXTRA_RESTART, true);
onStartCommand(intent, 12345, 67890);
} else if(lastState == State.MANUAL_QUITTING || lastState == State.GRACEFUL_SHUTDOWN) {
synchronized(_stateLock) {
setState(State.MANUAL_QUITTED);
stopSelf(); // Die.
}
}
}
private BroadcastReceiver onStateRequested = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// Broadcast the current state within this app.
Intent ni = new Intent(LOCAL_BROADCAST_STATE_NOTIFICATION);
ni.putExtra(LOCAL_BROADCAST_EXTRA_STATE, (android.os.Parcelable) _state);
LocalBroadcastManager.getInstance(RouterService.this).sendBroadcast(ni);
}
};
private BroadcastReceiver onLocaleChanged = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
localeManager.updateServiceLocale(RouterService.this);
}
};
/**
* NOT called by system if it restarts us after a crash
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Util.d(this + " onStart called"
+ " Intent is: " + intent
+ " Flags is: " + flags
+ " ID is: " + startId
+ " Current state is: " + _state);
boolean restart = intent != null && intent.getBooleanExtra(EXTRA_RESTART, false);
if(restart) {
Util.d(this + " RESTARTING");
}
synchronized(_stateLock) {
if(_state != State.INIT) //return START_STICKY;
{
return START_NOT_STICKY;
}
_receiver = new I2PReceiver(this);
if(Connectivity.isConnected(this)) {
if(restart) {
_statusBar.replace(StatusBar.ICON_STARTING, R.string.notification_status_restarting);
} else {
_statusBar.replace(StatusBar.ICON_STARTING, R.string.notification_status_starting);
}
setState(State.STARTING);
_starterThread = new Thread(new Starter());
_starterThread.start();
} else {
_statusBar.replace(StatusBar.ICON_WAITING_NETWORK, R.string.notification_status_waiting);
setState(State.WAITING);
_handler.postDelayed(new Waiter(), 10 * 1000);
}
}
_handler.removeCallbacks(_updater);
_handler.postDelayed(_updater, 50);
if(!restart) {
startForeground(1337, _statusBar.getNote());
}
//return START_STICKY;
return START_NOT_STICKY;
}
/**
* maybe this goes away when the receiver can bind to us
*/
private class Waiter implements Runnable {
public void run() {
Util.d(MARKER + this + " waiter handler"
+ " Current state is: " + _state);
if(_state == State.WAITING) {
if(Connectivity.isConnected(RouterService.this)) {
synchronized(_stateLock) {
if(_state != State.WAITING) {
return;
}
_statusBar.replace(StatusBar.ICON_STARTING, R.string.notification_status_starting_after_waiting);
setState(State.STARTING);
_starterThread = new Thread(new Starter());
_starterThread.start();
}
return;
}
_handler.postDelayed(this, 15 * 1000);
}
}
}
private class Starter implements Runnable {
public void run() {
Util.d(MARKER + this + " starter thread"
+ " Current state is: " + _state);
//Util.d(MARKER + this + " JBigI speed test started");
//NativeBigInteger.main(null);
//Util.d(MARKER + this + " JBigI speed test finished, launching router");
// Launch the router!
RouterLaunch.main(null);
synchronized(_stateLock) {
if(_state != State.STARTING) {
return;
}
setState(State.RUNNING);
_statusBar.replace(StatusBar.ICON_RUNNING, R.string.notification_status_running);
_context = Util.getRouterContext();
if (_context == null) {
throw new IllegalStateException("No contexts. This is usually because the router is either starting up or shutting down.");
}
_context.router().setKillVMOnEnd(false);
Job loadJob = new LoadClientsJob(RouterService.this, _context, _notif);
_context.jobQueue().addJob(loadJob);
_context.addShutdownTask(new ShutdownHook());
_context.addFinalShutdownTask(new FinalShutdownHook());
_starterThread = null;
}
Util.d("Router.main finished");
}
}
private class Updater implements Runnable {
public void run() {
RouterContext ctx = _context;
if(ctx != null && (_state == State.RUNNING || _state == State.ACTIVE || _state == State.GRACEFUL_SHUTDOWN)) {
Router router = ctx.router();
if(router.isAlive()) {
updateStatus(ctx);
}
}
_handler.postDelayed(this, 15 * 1000);
}
}
private String _currTitle;
private boolean _hadTunnels;
private void updateStatus(RouterContext ctx) {
int active = ctx.commSystem().countActivePeers();
int known = Math.max(ctx.netDb().getKnownRouters() - 1, 0);
int inEx = ctx.tunnelManager().getFreeTunnelCount();
int outEx = ctx.tunnelManager().getOutboundTunnelCount();
int inCl = ctx.tunnelManager().getInboundClientTunnelCount();
int outCl = ctx.tunnelManager().getOutboundClientTunnelCount();
String uptime = DataHelper.formatDuration(ctx.router().getUptime());
double inBW = ctx.bandwidthLimiter().getReceiveBps();
double outBW = ctx.bandwidthLimiter().getSendBps();
String text =
getResources().getString(R.string.notification_status_text,
Util.formatSpeed(inBW), Util.formatSpeed(outBW));
String bigText =
getResources().getString(R.string.notification_status_bw,
Util.formatSpeed(inBW), Util.formatSpeed(outBW)) + '\n'
+ getResources().getString(R.string.notification_status_peers,
active, known) + '\n'
+ getResources().getString(R.string.notification_status_expl,
inEx, outEx) + '\n'
+ getResources().getString(R.string.notification_status_client,
inCl, outCl);
boolean haveTunnels = inCl > 0 && outCl > 0;
if (isGracefulShutdownInProgress()) {
long ms = ctx.router().getShutdownTimeRemaining();
if (ms > 1000) {
_currTitle = getString(R.string.notification_status_graceful, DataHelper.formatDuration(ms));
} else {
_currTitle = getString(R.string.notification_status_stopping);
}
} else if (haveTunnels != _hadTunnels) {
if(haveTunnels) {
_currTitle = getString(R.string.notification_status_client_ready);
setState(State.ACTIVE);
_statusBar.replace(StatusBar.ICON_ACTIVE, _currTitle);
} else {
_currTitle = getString(R.string.notification_status_client_down);
setState(State.RUNNING);
_statusBar.replace(StatusBar.ICON_RUNNING, _currTitle);
}
_hadTunnels = haveTunnels;
} else if (_currTitle == null || _currTitle.equals(""))
_currTitle = getString(R.string.notification_status_running);
_statusBar.update(_currTitle, text, bigText);
}
@Override
public IBinder onBind(Intent intent) {
Util.d(this + "onBind called"
+ " Current state is: " + _state);
Util.d("Intent action: " + intent.getAction());
// Select the interface to return.
if (RouterBinder.class.getName().equals(intent.getAction())) {
// Local Activity wanting access to the RouterContext
Util.d("Returning RouterContext binder");
return _binder;
}
if (IRouterState.class.getName().equals(intent.getAction())) {
// Someone wants to monitor the router state.
Util.d("Returning state binder");
return mStatusBinder;
}
Util.d("Unknown binder request, returning null");
return null;
}
/**
* IRouterState is defined through IDL
*/
private final IRouterState.Stub mStatusBinder = new IRouterState.Stub() {
public void registerCallback(IRouterStateCallback cb)
throws RemoteException {
if (cb != null) mStateCallbacks.register(cb);
}
public void unregisterCallback(IRouterStateCallback cb)
throws RemoteException {
if (cb != null) mStateCallbacks.unregister(cb);
}
public boolean isStarted() throws RemoteException {
return _state != State.INIT &&
_state != State.STOPPED &&
_state != State.MANUAL_STOPPED &&
_state != State.MANUAL_QUITTED;
}
public State getState() throws RemoteException {
return _state;
}
};
@Override
public boolean onUnbind(Intent intent) {
return super.onUnbind(intent);
}
// ******** following methods may be accessed from Activities and Receivers ************
/**
* @return null if router is not running
*/
public RouterContext getRouterContext() {
RouterContext rv = _context;
if(rv == null) {
return null;
}
if(!rv.router().isAlive()) {
return null;
}
if(_state != State.RUNNING
&& _state != State.ACTIVE
&& _state != State.STOPPING
&& _state != State.MANUAL_STOPPING
&& _state != State.MANUAL_QUITTING
&& _state != State.NETWORK_STOPPING
&& _state != State.GRACEFUL_SHUTDOWN) {
return null;
}
return rv;
}
/**
* debug
*/
public String getState() {
return _state.toString();
}
public boolean canManualStop() {
return _state == State.WAITING ||
_state == State.RUNNING || _state == State.ACTIVE ||
_state == State.GRACEFUL_SHUTDOWN;
}
/**
* Stop and don't restart the router, but keep the service
*
* Apparently unused - see manualQuit()
*/
public void manualStop() {
Util.d("manualStop called"
+ " Current state is: " + _state);
synchronized(_stateLock) {
if(!canManualStop()) {
return;
}
if(_state == State.STARTING) {
_starterThread.interrupt();
}
if(_state == State.STARTING || _state == State.RUNNING ||
_state == State.ACTIVE || _state == State.GRACEFUL_SHUTDOWN) {
_statusBar.replace(StatusBar.ICON_STOPPING, R.string.notification_status_stopping);
Thread stopperThread = new Thread(new Stopper(State.MANUAL_STOPPING, State.MANUAL_STOPPED));
stopperThread.start();
}
}
}
/**
* Stop the router and kill the service
*/
public void manualQuit() {
Util.d("manualQuit called"
+ " Current state is: " + _state);
synchronized(_stateLock) {
if(!canManualStop()) {
return;
}
if(_state == State.STARTING) {
_starterThread.interrupt();
}
if(_state == State.STARTING || _state == State.RUNNING ||
_state == State.ACTIVE || _state == State.GRACEFUL_SHUTDOWN) {
_statusBar.replace(StatusBar.ICON_STOPPING, R.string.notification_status_stopping);
Thread stopperThread = new Thread(new Stopper(State.MANUAL_QUITTING, State.MANUAL_QUITTED));
stopperThread.start();
} else if(_state == State.WAITING) {
setState(State.MANUAL_QUITTING);
(new FinalShutdownHook()).run();
}
}
}
/**
* Stop and then spin waiting for a network connection, then restart
*/
public void networkStop() {
Util.d("networkStop called"
+ " Current state is: " + _state);
synchronized(_stateLock) {
if(_state == State.STARTING) {
_starterThread.interrupt();
}
if(_state == State.STARTING || _state == State.RUNNING ||
_state == State.ACTIVE || _state == State.GRACEFUL_SHUTDOWN) {
_statusBar.replace(StatusBar.ICON_STOPPING, R.string.notification_status_stopping_after_net);
// don't change state, let the shutdown hook do it
Thread stopperThread = new Thread(new Stopper(State.NETWORK_STOPPING, State.NETWORK_STOPPING));
stopperThread.start();
}
}
}
public boolean canManualStart() {
// We can be in INIT if we restarted after crash but previous state was not RUNNING.
return _state == State.INIT || _state == State.MANUAL_STOPPED || _state == State.STOPPED;
}
public void manualStart() {
Util.d("restart called"
+ " Current state is: " + _state);
synchronized(_stateLock) {
if(!canManualStart()) {
return;
}
_statusBar.replace(StatusBar.ICON_STARTING, R.string.notification_status_starting);
setState(State.STARTING);
_starterThread = new Thread(new Starter());
_starterThread.start();
}
}
/**
* Graceful Shutdown
*
* @since 0.9.19
*/
public boolean isGracefulShutdownInProgress() {
if (_state == State.GRACEFUL_SHUTDOWN) {
RouterContext ctx = _context;
return ctx != null && ctx.router().gracefulShutdownInProgress();
}
return false;
}
private String _oldTitle;
/**
* Graceful Shutdown
*
* @since 0.9.19
*/
public void gracefulShutdown() {
Util.d("gracefulShutdown called"
+ " Current state is: " + _state);
synchronized(_stateLock) {
if(!canManualStop()) {
return;
}
if(_state == State.STARTING || _state == State.WAITING) {
manualQuit();
return;
}
if(_state == State.RUNNING || _state == State.ACTIVE) {
RouterContext ctx = _context;
if(ctx != null && ctx.router().isAlive()) {
int part = ctx.tunnelManager().getParticipatingCount();
if(part <= 0) {
manualQuit();
} else {
ctx.router().shutdownGracefully();
_oldTitle = _currTitle;
long ms = ctx.router().getShutdownTimeRemaining();
if (ms > 1000) {
_statusBar.replace(
StatusBar.ICON_STOPPING,
getString(R.string.notification_status_graceful, DataHelper.formatDuration(ms))
);
} else {
_statusBar.replace(StatusBar.ICON_STOPPING, R.string.notification_status_stopping);
}
setState(State.GRACEFUL_SHUTDOWN);
}
}
}
}
}
/**
* Cancel Graceful Shutdown
*
* @since 0.9.19
*/
public void cancelGracefulShutdown() {
Util.d("cancelGracefulShutdown called"
+ " Current state is: " + _state);
synchronized(_stateLock) {
if(_state != State.GRACEFUL_SHUTDOWN) {
return;
}
RouterContext ctx = _context;
if(ctx != null && ctx.router().isAlive()) {
ctx.router().cancelGracefulShutdown();
_currTitle = _oldTitle;
if (_hadTunnels) {
setState(State.ACTIVE);
_statusBar.replace(StatusBar.ICON_ACTIVE, R.string.notification_status_shutdown_cancelled);
} else {
setState(State.RUNNING);
_statusBar.replace(StatusBar.ICON_RUNNING, R.string.notification_status_shutdown_cancelled);
}
}
}
}
// ******** end methods accessed from Activities and Receivers ************
private static final int STATE_MSG = 1;
/**
* Our Handler used to execute operations on the main thread.
*/
private final Handler mHandler = new StateHandler(new WeakReference<>(this));
private static class StateHandler extends Handler {
WeakReference<RouterService> mReference;
public StateHandler(WeakReference<RouterService> reference) {
mReference = reference;
}
@Override
public void handleMessage(Message msg) {
RouterService parent = mReference.get();
if (parent == null)
return;
switch (msg.what) {
case STATE_MSG:
final State state = parent._state;
// Broadcast to all clients the new state.
final int N = parent.mStateCallbacks.beginBroadcast();
for (int i = 0; i < N; i++) {
try {
parent.mStateCallbacks.getBroadcastItem(i).stateChanged(state);
} catch (RemoteException e) {
// The RemoteCallbackList will take care of removing
// the dead object for us.
}
}
parent.mStateCallbacks.finishBroadcast();
break;
default:
super.handleMessage(msg);
}
}
}
/**
* Turn off the status bar. Unregister the receiver. If we were running,
* fire up the Stopper thread.
*/
@Override
public void onDestroy() {
Util.d("onDestroy called"
+ " Current state is: " + _state);
_handler.removeCallbacks(_updater);
_statusBar.remove();
LocalBroadcastManager.getInstance(this).unregisterReceiver(onStateRequested);
LocalBroadcastManager.getInstance(this).unregisterReceiver(onLocaleChanged);
I2PReceiver rcvr = _receiver;
if(rcvr != null) {
synchronized(rcvr) {
try {
// throws if not registered
unregisterReceiver(rcvr);
} catch(IllegalArgumentException iae) {
}
//rcvr.unbindRouter();
//_receiver = null;
}
}
synchronized(_stateLock) {
if(_state == State.STARTING) {
_starterThread.interrupt();
}
if(_state == State.STARTING || _state == State.RUNNING ||
_state == State.ACTIVE || _state == State.GRACEFUL_SHUTDOWN) {
// should this be in a thread?
_statusBar.replace(StatusBar.ICON_SHUTTING_DOWN, R.string.notification_status_shutting_down);
Thread stopperThread = new Thread(new Stopper(State.STOPPING, State.STOPPED));
stopperThread.start();
}
}
}
/**
* Transition to the next state. If we still have a context, shut down the
* router. Turn off the status bar. Then transition to the stop state.
*/
private class Stopper implements Runnable {
private final State nextState;
private final State stopState;
/**
* call holding statelock
*/
public Stopper(State next, State stop) {
nextState = next;
stopState = stop;
setState(next);
}
public void run() {
try {
Util.d(MARKER + this + " stopper thread"
+ " Current state is: " + _state);
RouterContext ctx = _context;
if(ctx != null) {
ctx.router().shutdown(Router.EXIT_HARD);
}
_statusBar.remove();
Util.d("********** Router shutdown complete");
synchronized(_stateLock) {
if(_state == nextState) {
setState(stopState);
}
}
} finally {
stopForeground(true);
_statusBar.remove();
}
}
}
/**
* First (early) hook. Update the status bar. Unregister the receiver.
*/
private class ShutdownHook implements Runnable {
public void run() {
Util.d(this + " shutdown hook"
+ " Current state is: " + _state);
_statusBar.replace(StatusBar.ICON_SHUTTING_DOWN, R.string.notification_status_shutting_down);
I2PReceiver rcvr = _receiver;
if(rcvr != null) {
synchronized(rcvr) {
try {
// throws if not registered
unregisterReceiver(rcvr);
} catch(IllegalArgumentException iae) {
}
//rcvr.unbindRouter();
//_receiver = null;
}
}
synchronized(_stateLock) {
// null out to release the memory
_context = null;
if(_state == State.STARTING) {
_starterThread.interrupt();
}
if(_state == State.WAITING || _state == State.STARTING
|| _state == State.RUNNING || _state == State.ACTIVE
|| _state == State.GRACEFUL_SHUTDOWN) {
setState(State.STOPPING);
}
}
}
}
/**
* Second (late) hook. Turn off the status bar. Null out the context. If we
* were stopped manually, do nothing. If we were stopped because of no
* network, start the waiter thread. If it stopped of unknown causes or from
* manualQuit(), kill the Service.
*/
private class FinalShutdownHook implements Runnable {
public void run() {
try {
Util.d(this + " final shutdown hook"
+ " Current state is: " + _state);
//I2PReceiver rcvr = _receiver;
synchronized(_stateLock) {
// null out to release the memory
_context = null;
Runtime.getRuntime().gc();
if(_state == State.STARTING) {
_starterThread.interrupt();
}
if(_state == State.MANUAL_STOPPING) {
setState(State.MANUAL_STOPPED);
} else if(_state == State.NETWORK_STOPPING) {
// start waiter handler
setState(State.WAITING);
_handler.postDelayed(new Waiter(), 10 * 1000);
} else if(_state == State.STARTING || _state == State.RUNNING
|| _state == State.ACTIVE || _state == State.STOPPING) {
Util.w(this + " died of unknown causes");
setState(State.STOPPED);
// Unregister all callbacks.
mStateCallbacks.kill();
stopForeground(true);
stopSelf();
} else if(_state == State.MANUAL_QUITTING || _state == State.GRACEFUL_SHUTDOWN) {
setState(State.MANUAL_QUITTED);
// Unregister all callbacks.
mStateCallbacks.kill();
stopForeground(true);
stopSelf();
}
}
} finally {
_statusBar.remove();
}
}
}
private State getSavedState() {
SharedPreferences prefs = getSharedPreferences(SHARED_PREFS, 0);
String stateString = prefs.getString(LAST_STATE, State.INIT.toString());
try {
return State.valueOf(stateString);
} catch (IllegalArgumentException e) {
return State.INIT;
}
}
private void setState(State s) {
_state = s;
saveState();
// Broadcast the new state within this app.
Intent intent = new Intent(LOCAL_BROADCAST_STATE_CHANGED);
intent.putExtra(LOCAL_BROADCAST_EXTRA_STATE, (android.os.Parcelable) _state);
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
// Notify other apps that the state has changed
mHandler.sendEmptyMessage(STATE_MSG);
}
/**
* @return success
*/
private boolean saveState() {
SharedPreferences prefs = getSharedPreferences(SHARED_PREFS, 0);
SharedPreferences.Editor edit = prefs.edit();
edit.putString(LAST_STATE, _state.toString());
return edit.commit();
}
}