diff --git a/android/.gitignore b/android/.gitignore index d9fa5a57..90cd315e 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -5,4 +5,11 @@ ant.properties local.properties build.sh bin -log* \ No newline at end of file +log* +.gradle +android.iml +build +gradle +gradlew +gradlew.bat + diff --git a/android/res/values/strings.xml b/android/res/values/strings.xml index c147a808..1421b261 100755 --- a/android/res/values/strings.xml +++ b/android/res/values/strings.xml @@ -1,12 +1,17 @@ i2pd - i2pd started - i2pd service started - i2pd service stopped Stop Graceful Stop Graceful stop is already in progress Graceful stop is in progress Already stopped + i2pd initializing + i2pd is starting + i2pd: loaded JNI libraries + i2pd started + i2pd start failed + i2pd: graceful shutdown in progress + i2pd has stopped + remaining diff --git a/android/src/org/purplei2p/i2pd/DaemonSingleton.java b/android/src/org/purplei2p/i2pd/DaemonSingleton.java index 64568f83..4f3e62f7 100644 --- a/android/src/org/purplei2p/i2pd/DaemonSingleton.java +++ b/android/src/org/purplei2p/i2pd/DaemonSingleton.java @@ -8,8 +8,8 @@ import android.util.Log; public class DaemonSingleton { private static final String TAG="i2pd"; private static final DaemonSingleton instance = new DaemonSingleton(); - public static interface StateUpdateListener { void daemonStateUpdate(); } - private final Set stateUpdateListeners = new HashSet(); + public interface StateUpdateListener { void daemonStateUpdate(); } + private final Set stateUpdateListeners = new HashSet<>(); public static DaemonSingleton getInstance() { return instance; @@ -18,63 +18,72 @@ public class DaemonSingleton { public synchronized void addStateChangeListener(StateUpdateListener listener) { stateUpdateListeners.add(listener); } public synchronized void removeStateChangeListener(StateUpdateListener listener) { stateUpdateListeners.remove(listener); } + private synchronized void setState(State newState) { + if(newState==null)throw new NullPointerException(); + State oldState = state; + if(oldState==null)throw new NullPointerException(); + if(oldState.equals(newState))return; + state=newState; + fireStateUpdate1(); + } public synchronized void stopAcceptingTunnels() { if(isStartedOkay()){ - state=State.gracefulShutdownInProgress; - fireStateUpdate(); + setState(State.gracefulShutdownInProgress); I2PD_JNI.stopAcceptingTunnels(); } } - public void onNetworkStateChange(boolean isConnected) { - I2PD_JNI.onNetworkStateChanged(isConnected); - } + private volatile boolean startedOkay; + + public enum State { + uninitialized(R.string.uninitialized), + starting(R.string.starting), + jniLibraryLoaded(R.string.jniLibraryLoaded), + startedOkay(R.string.startedOkay), + startFailed(R.string.startFailed), + gracefulShutdownInProgress(R.string.gracefulShutdownInProgress), + stopped(R.string.stopped); - private boolean startedOkay; + State(int statusStringResourceId) { + this.statusStringResourceId = statusStringResourceId; + } - public static enum State {uninitialized,starting,jniLibraryLoaded,startedOkay,startFailed,gracefulShutdownInProgress,stopped}; + private final int statusStringResourceId; - private State state = State.uninitialized; + public int getStatusStringResourceId() { + return statusStringResourceId; + } + }; + + private volatile State state = State.uninitialized; public State getState() { return state; } - public synchronized void start() { - if(state != State.uninitialized)return; - state = State.starting; - fireStateUpdate(); + { + setState(State.starting); new Thread(new Runnable(){ @Override public void run() { try { I2PD_JNI.loadLibraries(); - synchronized (DaemonSingleton.this) { - state = State.jniLibraryLoaded; - fireStateUpdate(); - } + setState(State.jniLibraryLoaded); } catch (Throwable tr) { lastThrowable=tr; - synchronized (DaemonSingleton.this) { - state = State.startFailed; - fireStateUpdate(); - } + setState(State.startFailed); return; } try { synchronized (DaemonSingleton.this) { daemonStartResult = I2PD_JNI.startDaemon(); if("ok".equals(daemonStartResult)){ - state=State.startedOkay; + setState(State.startedOkay); setStartedOkay(true); - }else state=State.startFailed; - fireStateUpdate(); + }else setState(State.startFailed); } } catch (Throwable tr) { lastThrowable=tr; - synchronized (DaemonSingleton.this) { - state = State.startFailed; - fireStateUpdate(); - } + setState(State.startFailed); return; } } @@ -84,7 +93,7 @@ public class DaemonSingleton { private Throwable lastThrowable; private String daemonStartResult="N/A"; - private synchronized void fireStateUpdate() { + private void fireStateUpdate1() { Log.i(TAG, "daemon state change: "+state); for(StateUpdateListener listener : stateUpdateListeners) { try { @@ -121,10 +130,7 @@ public class DaemonSingleton { if(isStartedOkay()){ try {I2PD_JNI.stopDaemon();}catch(Throwable tr){Log.e(TAG, "", tr);} setStartedOkay(false); - synchronized (DaemonSingleton.this) { - state = State.stopped; - fireStateUpdate(); - } + setState(State.stopped); } } } diff --git a/android/src/org/purplei2p/i2pd/ForegroundService.java b/android/src/org/purplei2p/i2pd/ForegroundService.java index 74761b07..6116b982 100644 --- a/android/src/org/purplei2p/i2pd/ForegroundService.java +++ b/android/src/org/purplei2p/i2pd/ForegroundService.java @@ -11,11 +11,32 @@ import android.util.Log; import android.widget.Toast; public class ForegroundService extends Service { + private static final String TAG="FgService"; + + private volatile boolean shown; + + private final DaemonSingleton.StateUpdateListener daemonStateUpdatedListener = + new DaemonSingleton.StateUpdateListener() { + + @Override + public void daemonStateUpdate() { + try { + synchronized (ForegroundService.this) { + if (shown) cancelNotification(); + showNotification(); + } + } catch (Throwable tr) { + Log.e(TAG,"error ignored",tr); + } + } + }; + + private NotificationManager notificationManager; // Unique Identification Number for the Notification. // We use it on Notification start, and to cancel it. - private int NOTIFICATION = R.string.i2pd_started; + private int NOTIFICATION = 1; /** * Class for clients to access. Because we know this service always @@ -32,29 +53,35 @@ public class ForegroundService extends Service { public void onCreate() { notificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); - // Display a notification about us starting. We put an icon in the status bar. - showNotification(); - daemon.start(); + synchronized (this) { + DaemonSingleton.getInstance().addStateChangeListener(daemonStateUpdatedListener); + if (!shown) daemonStateUpdatedListener.daemonStateUpdate(); + } // Tell the user we started. - Toast.makeText(this, R.string.i2pd_service_started, Toast.LENGTH_SHORT).show(); +// Toast.makeText(this, R.string.i2pd_service_started, Toast.LENGTH_SHORT).show(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.i("ForegroundService", "Received start id " + startId + ": " + intent); - daemon.start(); return START_STICKY; } @Override public void onDestroy() { + DaemonSingleton.getInstance().removeStateChangeListener(daemonStateUpdatedListener); + cancelNotification(); + } + + private synchronized void cancelNotification() { // Cancel the persistent notification. notificationManager.cancel(NOTIFICATION); stopForeground(true); // Tell the user we stopped. - Toast.makeText(this, R.string.i2pd_service_stopped, Toast.LENGTH_SHORT).show(); +// Toast.makeText(this, R.string.i2pd_service_stopped, Toast.LENGTH_SHORT).show(); + shown=false; } @Override @@ -69,9 +96,9 @@ public class ForegroundService extends Service { /** * Show a notification while this service is running. */ - private void showNotification() { + private synchronized void showNotification() { // In this sample, we'll use the same text for the ticker and the expanded notification - CharSequence text = getText(R.string.i2pd_started); + CharSequence text = getText(DaemonSingleton.getInstance().getState().getStatusStringResourceId()); // The PendingIntent to launch our activity if the user selects this notification PendingIntent contentIntent = PendingIntent.getActivity(this, 0, @@ -90,8 +117,9 @@ public class ForegroundService extends Service { // Send the notification. //mNM.notify(NOTIFICATION, notification); startForeground(NOTIFICATION, notification); + shown=true; } - private final DaemonSingleton daemon = DaemonSingleton.getInstance(); + private static final DaemonSingleton daemon = DaemonSingleton.getInstance(); } diff --git a/android/src/org/purplei2p/i2pd/I2PDActivity.java b/android/src/org/purplei2p/i2pd/I2PDActivity.java index 36e992b3..99672eb7 100755 --- a/android/src/org/purplei2p/i2pd/I2PDActivity.java +++ b/android/src/org/purplei2p/i2pd/I2PDActivity.java @@ -19,13 +19,14 @@ import android.widget.TextView; import android.widget.Toast; public class I2PDActivity extends Activity { - private static final String TAG = "i2pd"; + private static final String TAG = "i2pdActvt"; + public static final int GRACEFUL_DELAY_MILLIS = 10 * 60 * 1000; - private TextView textView; + private TextView textView; - private final DaemonSingleton daemon = DaemonSingleton.getInstance(); + private static final DaemonSingleton daemon = DaemonSingleton.getInstance(); - private DaemonSingleton.StateUpdateListener daemonStateUpdatedListener = + private final DaemonSingleton.StateUpdateListener daemonStateUpdatedListener = new DaemonSingleton.StateUpdateListener() { @Override @@ -42,8 +43,11 @@ public class I2PDActivity extends Activity { return; } DaemonSingleton.State state = daemon.getState(); - textView.setText(String.valueOf(state)+ - (DaemonSingleton.State.startFailed.equals(state)?": "+daemon.getDaemonStartResult():"")); + textView.setText( + String.valueOf(state)+ + (DaemonSingleton.State.startFailed.equals(state)?": "+daemon.getDaemonStartResult():"")+ + (DaemonSingleton.State.gracefulShutdownInProgress.equals(state)?": "+formatGraceTimeRemaining()+" "+getText(R.string.remaining):"") + ); } catch (Throwable tr) { Log.e(TAG,"error ignored",tr); } @@ -51,6 +55,18 @@ public class I2PDActivity extends Activity { }); } }; + private static volatile long graceStartedMillis; + private static final Object graceStartedMillis_LOCK=new Object(); + + private static String formatGraceTimeRemaining() { + long remainingSeconds; + synchronized (graceStartedMillis_LOCK){ + remainingSeconds=Math.round(Math.max(0,graceStartedMillis+GRACEFUL_DELAY_MILLIS-System.currentTimeMillis())/1000.0D); + } + long remainingMinutes=(long)Math.floor(remainingSeconds/60.0D); + long remSec=remainingSeconds-remainingMinutes*60; + return remainingMinutes+":"+(remSec/10)+remSec%10; + } @Override public void onCreate(Bundle savedInstanceState) { @@ -58,35 +74,44 @@ public class I2PDActivity extends Activity { textView = new TextView(this); setContentView(textView); - DaemonSingleton.getInstance().addStateChangeListener(daemonStateUpdatedListener); + daemon.addStateChangeListener(daemonStateUpdatedListener); daemonStateUpdatedListener.daemonStateUpdate(); //set the app be foreground doBindService(); + + final Timer gracefulQuitTimer = getGracefulQuitTimer(); + if(gracefulQuitTimer!=null){ + long gracefulStopAtMillis; + synchronized (graceStartedMillis_LOCK) { + gracefulStopAtMillis = graceStartedMillis + GRACEFUL_DELAY_MILLIS; + } + rescheduleGraceStop(gracefulQuitTimer, gracefulStopAtMillis); + } } @Override protected void onDestroy() { super.onDestroy(); - localDestroy(); - } - - private void localDestroy() { - textView = null; - DaemonSingleton.getInstance().removeStateChangeListener(daemonStateUpdatedListener); - Timer gracefulQuitTimer = getGracefulQuitTimer(); - if(gracefulQuitTimer!=null) { - gracefulQuitTimer.cancel(); - setGracefulQuitTimer(null); + textView = null; + daemon.removeStateChangeListener(daemonStateUpdatedListener); + //cancelGracefulStop(); + try{ + doUnbindService(); + }catch(Throwable tr){ + Log.e(TAG, "", tr); } -// try{ -// doUnbindService(); -// }catch(Throwable tr){ -// Log.e(TAG, "", tr); -// } } - private CharSequence throwableToString(Throwable tr) { + private static void cancelGracefulStop() { + Timer gracefulQuitTimer = getGracefulQuitTimer(); + if(gracefulQuitTimer!=null) { + gracefulQuitTimer.cancel(); + setGracefulQuitTimer(null); + } + } + + private CharSequence throwableToString(Throwable tr) { StringWriter sw = new StringWriter(8192); PrintWriter pw = new PrintWriter(sw); tr.printStackTrace(pw); @@ -122,24 +147,27 @@ public class I2PDActivity extends Activity { }; - private boolean mIsBound; + private static volatile boolean mIsBound; - private synchronized void doBindService() { - if(mIsBound)return; - // Establish a connection with the service. We use an explicit - // class name because we want a specific service implementation that - // we know will be running in our own process (and thus won't be - // supporting component replacement by other applications). - bindService(new Intent(this, - ForegroundService.class), mConnection, Context.BIND_AUTO_CREATE); - mIsBound = true; + private void doBindService() { + synchronized (I2PDActivity.class) { + if (mIsBound) return; + // Establish a connection with the service. We use an explicit + // class name because we want a specific service implementation that + // we know will be running in our own process (and thus won't be + // supporting component replacement by other applications). + bindService(new Intent(this, ForegroundService.class), mConnection, Context.BIND_AUTO_CREATE); + mIsBound = true; + } } private void doUnbindService() { - if (mIsBound) { - // Detach our existing connection. - unbindService(mConnection); - mIsBound = false; + synchronized (I2PDActivity.class) { + if (mIsBound) { + // Detach our existing connection. + unbindService(mConnection); + mIsBound = false; + } } } @@ -170,16 +198,25 @@ public class I2PDActivity extends Activity { } private void i2pdStop() { - try{ - daemon.stopDaemon(); - }catch (Throwable tr) { - Log.e(TAG, "", tr); - } + cancelGracefulStop(); + new Thread(new Runnable(){ + + @Override + public void run() { + Log.d(TAG, "stopping"); + try{ + daemon.stopDaemon(); + }catch (Throwable tr) { + Log.e(TAG, "", tr); + } + } + + },"stop").start(); } - private Timer gracefulQuitTimer; - private final Object gracefulQuitTimerLock = new Object(); - private synchronized void i2pdGracefulStop() { + private static volatile Timer gracefulQuitTimer; + + private void i2pdGracefulStop() { if(daemon.getState()==DaemonSingleton.State.stopped){ Toast.makeText(this, R.string.already_stopped, Toast.LENGTH_SHORT).show(); @@ -200,16 +237,12 @@ public class I2PDActivity extends Activity { Log.d(TAG, "grac stopping"); if(daemon.isStartedOkay()) { daemon.stopAcceptingTunnels(); - Timer gracefulQuitTimer = new Timer(true); - setGracefulQuitTimer(gracefulQuitTimer); - gracefulQuitTimer.schedule(new TimerTask(){ - - @Override - public void run() { - i2pdStop(); - } - - }, 10*60*1000/*milliseconds*/); + long gracefulStopAtMillis; + synchronized (graceStartedMillis_LOCK) { + graceStartedMillis = System.currentTimeMillis(); + gracefulStopAtMillis = graceStartedMillis + GRACEFUL_DELAY_MILLIS; + } + rescheduleGraceStop(null,gracefulStopAtMillis); }else{ i2pdStop(); } @@ -218,18 +251,35 @@ public class I2PDActivity extends Activity { } } - },"gracQuitInit").start(); + },"gracInit").start(); } - private Timer getGracefulQuitTimer() { - synchronized (gracefulQuitTimerLock) { - return gracefulQuitTimer; - } + private void rescheduleGraceStop(Timer gracefulQuitTimerOld, long gracefulStopAtMillis) { + if(gracefulQuitTimerOld!=null)gracefulQuitTimerOld.cancel(); + final Timer gracefulQuitTimer = new Timer(true); + setGracefulQuitTimer(gracefulQuitTimer); + gracefulQuitTimer.schedule(new TimerTask(){ + + @Override + public void run() { + i2pdStop(); + } + + }, Math.max(0,gracefulStopAtMillis-System.currentTimeMillis())); + final TimerTask tickerTask = new TimerTask() { + @Override + public void run() { + daemonStateUpdatedListener.daemonStateUpdate(); + } + }; + gracefulQuitTimer.scheduleAtFixedRate(tickerTask,0/*start delay*/,1000/*millis period*/); + } + + private static Timer getGracefulQuitTimer() { + return gracefulQuitTimer; } - private void setGracefulQuitTimer(Timer gracefulQuitTimer) { - synchronized (gracefulQuitTimerLock) { - this.gracefulQuitTimer = gracefulQuitTimer; - } + private static void setGracefulQuitTimer(Timer gracefulQuitTimer) { + I2PDActivity.gracefulQuitTimer = gracefulQuitTimer; } }